# Copyright (c) 2015 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

import os
import uuid


class AbspathInvalidError(Exception):
  """Raised if an abspath cannot be sanitized based on an app's source paths."""


class UserFriendlyStringInvalidError(Exception):
  """Raised if a user friendly string cannot be parsed."""


class ModuleToLoad(object):

  def __init__(self, href=None, filename=None):
    if bool(href) == bool(filename):
      raise Exception('ModuleToLoad must specify exactly one of href and '
                      'filename.')
    self.href = href
    self.filename = filename

  def __repr__(self):
    if self.href:
      return 'ModuleToLoad(href="%s")' % self.href
    return 'ModuleToLoad(filename="%s")' % self.filename

  def AsDict(self):
    if self.href:
      return {'href': self.href}
    return {'filename': self.filename}

  @staticmethod
  def FromDict(module_dict):
    return ModuleToLoad(module_dict.get('href'), module_dict.get('filename'))


class FunctionHandle(object):

  def __init__(self, modules_to_load=None, function_name=None,
               options=None, guid=uuid.uuid4()):
    self.modules_to_load = modules_to_load
    self.function_name = function_name
    self.options = options
    self._guid = guid

  def __repr__(self):
    return 'FunctionHandle(modules_to_load=[%s], function_name="%s")' % (
        ', '.join([str(module) for module in self.modules_to_load]),
        self.function_name)

  @property
  def guid(self):
    return self._guid

  @property
  def has_hrefs(self):
    return any(module.href for module in self.modules_to_load)

  def AsDict(self):
    handle_dict = {
        'function_name': self.function_name
    }

    if self.modules_to_load is not None:
      handle_dict['modules_to_load'] = [module.AsDict() for module in
                                        self.modules_to_load]
    if self.options is not None:
      handle_dict['options'] = self.options

    return handle_dict

  def ConvertHrefsToAbsFilenames(self, app):
    """Converts hrefs to absolute filenames in the context of |app|.

    In an app-serving context, functions must only reside in files which the app
    is serving, in order to prevent directory traversal attacks. In addition, we
    rely on paths being absolute when actually executing functions.

    Args:
      app: A dev server instance requesting abspath conversion.

    Returns:
      A new FunctionHandle instance with no hrefs.

    Raises:
      AbspathInvalidError: If there is no source path with which a given abspath
          shares a common prefix.
    """
    new_modules_to_load = []
    for module in self.modules_to_load:
      if module.href:
        abspath = app.GetAbsFilenameForHref(module.href)
      else:
        assert os.path.abspath(module.filename) == module.filename
        abspath = module.filename

      if not abspath:
        raise AbspathInvalidError('Filename %s invalid', abspath)

      new_modules_to_load.append(ModuleToLoad(filename=abspath))

    return FunctionHandle(modules_to_load=new_modules_to_load,
                          function_name=self.function_name)

  @staticmethod
  def FromDict(handle_dict):
    if handle_dict.get('modules_to_load') is not None:
      modules_to_load = [ModuleToLoad.FromDict(module_dict) for module_dict in
                         handle_dict['modules_to_load']]
    else:
      modules_to_load = []
    options = handle_dict.get('options')
    return FunctionHandle(modules_to_load=modules_to_load,
                          function_name=handle_dict['function_name'],
                          options=options)

  def AsUserFriendlyString(self, app):
    parts = [module.filename for module in
             self.ConvertHrefsToAbsFilenames(app).modules_to_load]
    parts.append(self.function_name)

    return ':'.join(parts)

  @staticmethod
  def FromUserFriendlyString(user_str):
    parts = user_str.split(':')
    if len(parts) < 2:
      raise UserFriendlyStringInvalidError(
          'Tried to deserialize string with less than two parts: ' + user_str)

    modules_to_load = [ModuleToLoad(filename=name) for name in parts[:-1]]

    return FunctionHandle(modules_to_load=modules_to_load,
                          function_name=parts[-1])

