#
# Copyright 2016 The Cobalt Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
"""Classes related to building and installing platform-specific packages."""

import abc
import importlib
import logging
import os

import _env  # pylint: disable=unused-import
import starboard
from starboard.tools import platform


def _ImportModule(path, root_module, module_name=None):
  """Load a platform specific python module using importlib.

  Load the python module named |module_name| relative to |root_module|.
  Args:
    path: Path to the platform
    root_module: An already-loaded module
    module_name: Name of a python module to load. If None, load the platform
      directory as a python module.

  Returns:
    A module loaded with importlib.import_module
  Throws:
    ImportError if the module fails to be loaded.
  """
  # From the relative path to the |root_module|'s directory, construct a full
  # python package name and attempt to load it.
  relative_path = os.path.relpath(path, os.path.dirname(root_module.__file__))
  full_path = os.path.join(root_module.__name__, relative_path)

  # This normpath may collapse out the root module. This is generally OK as long
  # as we stay within the workspace, as _env will add the workspace root to the
  # system import path.
  components = os.path.normpath(full_path).split(os.sep)
  if module_name:
    components.append(module_name)
  full_package_name = '.'.join(components)
  return importlib.import_module(full_package_name)


def _GetPackageClass(platform_info):
  """Loads the package class associated with the given platform.PlatformInfo."""
  try:
    module = _ImportModule(platform_info.path, starboard)
  except ImportError as e:
    logging.debug('Failed to import module for platform %s with error: %s',
                  platform_info.name, e)
    return None

  if not hasattr(module, 'Package'):
    return None

  return module.Package


def _GetPlatformInfosDict():
  """Find Starboard ports that support building packages.

  Returns:
    A dict of [platform_name, Class] where Class inherits from PackageBase
  """
  packager_modules = {}
  for platform_name in platform.GetAll():
    platform_info = platform.Get(platform_name)
    # From the relative path to the starboard directory, construct a full
    # python package name and attempt to load it.
    package_class = _GetPackageClass(platform_info)
    if not package_class:
      continue
    # Populate a mapping from platform name to the module containing the
    # Package class.
    try:
      for supported_name in package_class.SupportedPlatforms():
        if supported_name in packager_modules:
          logging.warning('Packager for %s is defined in multiple modules.',
                          supported_name)
        else:
          packager_modules[supported_name] = platform_info
    except Exception as e:  # pylint: disable=broad-except
      # Catch all exceptions to avoid an error in one platform's Packager
      # halting the script for other platforms' packagers.
      logging.warning(
          'Exception iterating supported platform for platform '
          '%s: %s.', platform_info.name, e)

  return packager_modules


class PackageBase(object):
  """Represents a build package that exists on the local filesystem."""
  __metaclass__ = abc.ABCMeta

  def __init__(self, source_dir, output_dir):
    """Initialize common paths for building Packages.

    Args:
      source_dir: The directory containing the application to be packaged.
      output_dir: The directory into which the package should be placed.
    """
    self.source_dir = source_dir
    self.output_dir = output_dir

  @abc.abstractmethod
  def Install(self, targets=None):
    """Install the package to the specified list of targets.

    Args:
      targets: A list of targets to install the package to, or None on platforms
        that support installing to a default target.  This method can be
        overridden to implement platform-specific steps to install the package
        for that platform.
    """
    del targets

  @classmethod
  def AddArguments(cls, arg_parser):
    """Add platform-specific command-line arguments to the ArgumentParser.

    Platforms that require additional arguments to configure building a package
    can override this method and add them to |arg_parser|.
    Args:
      arg_parser: An ArgumentParser object.
    """
    del cls, arg_parser

  @classmethod
  def ExtractArguments(cls, options):
    """Extract arguments from an ArgumentParser's namespace object.

    Platforms that add additional arguments can override this method to extract
    the options and add them to a dict that will be passed to the Package
    constructor.
    Args:
      options: A namespace object returned from ArgumentParser.parse_args

    Returns:
      A dict of kwargs to be passed to the Package constructor.
    """
    del cls, options
    return {}


class Packager(object):
  """Top level class for building a package."""

  def __init__(self):
    self.platform_infos = _GetPlatformInfosDict()

  def SupportedPlatforms(self):
    """Get a list of platforms for which a package can be built."""
    return self.platform_infos.keys()

  def GetPlatformInfo(self, platform_name):
    return self.platform_infos.get(platform_name, None)

  def GetApplicationPackageInfo(self, platform_name, application_name):
    """Get application-specific packaging information."""
    platform_info = self.GetPlatformInfo(platform_name)
    try:
      return _ImportModule(platform_info.path, starboard,
                           '%s.package' % application_name)
    except ImportError as e:
      # No package parameters specified for this platform.
      logging.debug('Failed to import cobalt.package: %s', e)
    return None

  def BuildPackage(self, platform_name, source_dir, output_dir, **kwargs):
    """Build a package for the specified platform.

    Args:
      platform_name: The platform for which a package should be built.
      source_dir: The directory containing the application to be packaged.
      output_dir: The directory into which the package files should be placed.
      **kwargs: Platform-specific arguments.

    Returns:
      A PackageBase instance.
    """
    package_class = _GetPackageClass(self.platform_infos[platform_name])
    return package_class(source_dir=source_dir, output_dir=output_dir, **kwargs)

  def AddPlatformArguments(self, platform_name, argparser):
    package_class = _GetPackageClass(self.platform_infos[platform_name])
    package_class.AddArguments(argparser)

  def ExtractPlatformArguments(self, platform_name, options):
    package_class = _GetPackageClass(self.platform_infos[platform_name])
    return package_class.ExtractArguments(options)
