# ==================================================================================================
# Copyright 2012 Twitter, Inc.
# --------------------------------------------------------------------------------------------------
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this work except in compliance with the License.
# You may obtain a copy of the License in the LICENSE file, or 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.
# ==================================================================================================

import os

from twitter.common.collections import OrderedSet

from twitter.pants.base.build_environment import get_buildroot
from twitter.pants.base.build_manual import manual
from twitter.pants.base.parse_context import ParseContext
from twitter.pants.base.target import TargetDefinitionException


@manual.builddict()
class SourceRoot(object):
  """Allows registration of a source root for a set of targets.

  A source root is the base path sources for a particular language are found relative to.
  Generally compilers or interpreters for the source will expect sources relative to a base path
  and a source root allows calculation of the correct relative paths.

  E.g., a Java compiler probably expects to find ``.java`` files for
  ``package com.twitter.common.net`` in ``*something*/com/twitter/common/net``.
  The ``source_root`` command specifies that *something*.

  It is illegal to have nested source roots.
  """
  _ROOTS_BY_TYPE = {}
  _TYPES_BY_ROOT = {}
  _SEARCHED = set()

  @classmethod
  def reset(cls):
    """Reset all source roots to empty. Only intended for testing."""
    cls._ROOTS_BY_TYPE = {}
    cls._TYPES_BY_ROOT = {}
    cls._SEARCHED = set()

  @classmethod
  def find(cls, target):
    """Finds the source root for the given target.

    If none is registered, returns the parent directory of the target's BUILD file.
    """
    target_path = os.path.relpath(target.address.buildfile.parent_path, get_buildroot())

    def _find():
      for root_dir, types in cls._TYPES_BY_ROOT.items():
        if target_path.startswith(root_dir):  # The only candidate root for this target.
          # Validate the target type, if restrictions were specified.
          if types and not isinstance(target, tuple(types)):
            # TODO: Find a way to use the BUILD file aliases in the error message, instead
            # of target.__class__.__name__. E.g., java_tests instead of JavaTests.
            raise TargetDefinitionException(target,
                'Target type %s not allowed under %s' % (target.__class__.__name__, root_dir))
          return root_dir
      return None

    # Try already registered roots
    root = _find()
    if root:
      return root

    # Fall back to searching the ancestor path for a root.
    # TODO(John Sirois): We currently allow for organic growth of maven multi-module layout style
    # projects (for example) and do not require a global up-front registration of all source roots
    # and instead do lazy resolution here.  This allows for parse cycles that lead to surprising
    # runtime errors.  Re-consider allowing lazy source roots at all.
    for buildfile in reversed(target.address.buildfile.ancestors()):
      if buildfile not in cls._SEARCHED:
        ParseContext(buildfile).parse()
        cls._SEARCHED.add(buildfile)
        root = _find()
        if root:
          return root

    # Finally, resolve files relative to the BUILD file parent dir as the target base
    return target_path

  @classmethod
  def types(cls, root):
    """Returns the set of target types rooted at root."""
    return cls._TYPES_BY_ROOT[root]

  @classmethod
  def roots(cls, target_type):
    """Returns the set of roots for given target type."""
    return cls._ROOTS_BY_TYPE[target_type]

  @classmethod
  def all_roots(cls):
    """Returns a mapping from source roots to the associated target types."""
    return dict(cls._TYPES_BY_ROOT)

  @classmethod
  def register(cls, basedir, *allowed_target_types):
    """Registers the given basedir (relative to the buildroot) as a source root.

    :param string basedir: The base directory to resolve sources relative to.
    :param list allowed_target_types: Optional list of target types. If specified, we enforce that
      only targets of those types appear under this source root.
    """
    cls._register(basedir, *allowed_target_types)

  @classmethod
  def _register(cls, source_root_dir, *allowed_target_types):
    """Registers a source root.

    :source_root_dir The source root directory against which we resolve source paths,
                     relative to the build root.
    :allowed_target_types Optional list of target types. If specified, we enforce that
                          only targets of those types appear under this source root.
    """
    # Verify that source_root_dir doesn't reach outside buildroot.
    buildroot = get_buildroot()
    if source_root_dir.startswith(buildroot):
      abspath = os.path.normpath(source_root_dir)
    else:
      abspath = os.path.normpath(os.path.join(buildroot, source_root_dir))
    if not abspath.startswith(buildroot):
      raise ValueError('Source root %s is not under the build root %s' % (abspath, buildroot))
    source_root_dir = os.path.relpath(abspath, buildroot)

    types = cls._TYPES_BY_ROOT.get(source_root_dir)
    if types is None:
      types = OrderedSet()
      cls._TYPES_BY_ROOT[source_root_dir] = types

    for allowed_target_type in allowed_target_types:
      types.add(allowed_target_type)
      roots = cls._ROOTS_BY_TYPE.get(allowed_target_type)
      if roots is None:
        roots = OrderedSet()
        cls._ROOTS_BY_TYPE[allowed_target_type] = roots
      roots.add(source_root_dir)
