# coding=utf-8
# Copyright 2015 Pants project contributors (see CONTRIBUTORS.md).
# Licensed under the Apache License, Version 2.0 (see LICENSE).

from __future__ import (absolute_import, division, generators, nested_scopes, print_function,
                        unicode_literals, with_statement)

import logging
import os
from abc import abstractmethod

from twitter.common.collections import OrderedSet

from pants.backend.core.tasks.task import Task
from pants.base.address import Address
from pants.base.address_lookup_error import AddressLookupError
from pants.base.build_environment import get_buildroot
from pants.base.build_graph import sort_targets
from pants.base.exceptions import TaskError
from pants.base.workunit import WorkUnitLabel
from pants.util.dirutil import safe_rmtree, safe_walk
from pants.util.memo import memoized_property
from pants.util.meta import AbstractClass


logger = logging.getLogger(__name__)


class SimpleCodegenTask(Task):
  """A base-class for code generation for a single target language."""

  @classmethod
  def product_types(cls):
    # NB(gmalmquist): This is a hack copied from the old CodeGen base class to get the round manager
    # to properly run codegen before resolve and compile. It would be more correct to just have each
    # individual codegen class declare what languages it generates, but would cause problems with
    # scala. See https://rbcommons.com/s/twitter/r/2540/.
    return ['java', 'scala', 'python']

  @classmethod
  def register_options(cls, register):
    super(SimpleCodegenTask, cls).register_options(register)
    register('--allow-empty', action='store_true', default=True, fingerprint=True,
             help='Skip targets with no sources defined.',
             advanced=True)
    strategy_names = [strategy.name() for strategy in cls.supported_strategy_types()]
    if cls.forced_codegen_strategy() is None:
      register('--strategy', choices=strategy_names, fingerprint=True,
               default=strategy_names[0],
               help='Selects the compilation strategy to use. The "global" strategy uses a shared '
                    'global directory for all generated code, and the "isolated" strategy uses '
                    'per-target codegen directories.',
               advanced=True)
    if 'isolated' in strategy_names:
      register('--allow-dups', action='store_true', default=False, fingerprint=True,
               help='Allow multiple targets specifying the same sources when using the isolated '
                    'strategy. If duplicates are allowed, the logic of find_sources in '
                    'IsolatedCodegenStrategy will associate generated sources with '
                    'least-dependent targets that generate them.',
               advanced=True)

  @classmethod
  def get_fingerprint_strategy(cls):
    """Override this method to use a fingerprint strategy other than the default one.

    :return: a fingerprint strategy, or None to use the default strategy.
    """
    return None

  def synthetic_target_extra_dependencies(self, target):
    """Gets any extra dependencies generated synthetic targets should have.

    This method is optional for subclasses to implement, because some code generators may have no
    extra dependencies.
    :param Target target: the Target from which we are generating a synthetic Target. E.g., 'target'
    might be a JavaProtobufLibrary, whose corresponding synthetic Target would be a JavaLibrary.
    It may not be necessary to use this parameter depending on the details of the subclass.
    :return: a list of dependencies.
    """
    return []

  @property
  def synthetic_target_type(self):
    """The type of target this codegen task generates.

    For example, the target type for JaxbGen would simply be JavaLibrary.
    :return: a type (class) that inherits from Target.
    """
    raise NotImplementedError

  def is_gentarget(self, target):
    """Predicate which determines whether the target in question is relevant to this codegen task.

    E.g., the JaxbGen task considers JaxbLibrary targets to be relevant, and nothing else.
    :param Target target: The target to check.
    :return: True if this class can generate code for the given target, False otherwise.
    """
    raise NotImplementedError

  def execute_codegen(self, invalid_targets):
    """Generated code for the given list of targets.

    :param invalid_targets: an iterable of targets (a subset of codegen_targets()).
    """
    raise NotImplementedError

  def codegen_targets(self):
    """Finds codegen targets in the dependency graph.

    :return: an iterable of dependency targets.
    """
    return self.context.targets(self.is_gentarget)

  @classmethod
  def supported_strategy_types(cls):
    """The CodegenStrategy subclasses that this codegen task supports.

    This list is used to generate the options for the --strategy flag. The first item in the list
    is used as the default value.

    By default, this only supports the IsolatedCodegenStrategy. Subclasses which desire global
    generation should subclass the GlobalCodegenStrategy.
    :return: the list of types (classes, not instances) that extend from CodegenStrategy.
    :rtype: list
    """
    return [cls.IsolatedCodegenStrategy]

  @classmethod
  def forced_codegen_strategy(cls):
    """If only a single codegen strategy is supported, returns its name.

    This value of this function is automatically computed from the supported_strategy_types.
    :return: the forced code generation strategy, or None if multiple options are supported.
    """
    strategy_types = cls.supported_strategy_types()
    if not strategy_types:
      raise TaskError("{} doesn't support any codegen strategies.".format(cls.__name__))
    if len(strategy_types) == 1:
      return strategy_types[0].name()
    return None

  @classmethod
  def _codegen_strategy_map(cls):
    """Returns a dict which maps names to codegen strategy types.

    This is generated from the supported_strategy_types list.
    """
    return {strategy.name(): strategy for strategy in cls.supported_strategy_types()}

  def _codegen_strategy_for_name(self, name):
    strategy_type_map = self._codegen_strategy_map()
    if name not in strategy_type_map:
      raise self.UnsupportedStrategyError('Unsupported codegen strategy "{}".'.format(name))
    return strategy_type_map[name](self)

  @memoized_property
  def codegen_strategy(self):
    """Returns the codegen strategy object used by this codegen.

    This is controlled first by the forced_codegen_strategy method, then by user-specified
    options if the former returns None.

    If you just want the name ('global' or 'isolated') of the strategy, use codegen_strategy.name().

    :return: the codegen strategy object.
    :rtype: SimpleCodegenTask.CodegenStrategy
    """