# ==================================================================================================
# 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
import sys

from abc import abstractmethod
from collections import defaultdict

from twitter.common.lang import AbstractClass, Compatibility

from twitter.pants import get_buildroot, get_scm, has_resources, has_sources, TaskError
from twitter.pants.base.build_file import BuildFile
from twitter.pants.base.target import Target
from twitter.pants.scm import Scm
from twitter.pants.tasks.console_task import ConsoleTask


class WhatChanged(ConsoleTask):
  """Emits the targets that have been modified since a given commit."""

  @classmethod
  def setup_parser(cls, option_group, args, mkflag):
    super(WhatChanged, cls).setup_parser(option_group, args, mkflag)

    option_group.add_option(mkflag('parent'), dest='what_changed_create_prefix', default='HEAD',
                            help='[%default] Identifies the parent tree-ish to calculate changes '
                                 'against.')

    option_group.add_option(mkflag("files"), mkflag("files", negate=True), default=False,
                            action="callback", callback=mkflag.set_bool,
                            dest='what_changed_show_files',
                            help='[%default] Shows changed files instead of the targets that own '
                                 'them.')

  def __init__(self, context, workspace, outstream=sys.stdout):
    if not isinstance(workspace, Workspace):
      raise ValueError('WhatChanged requires a Workspace, given %s' % workspace)

    super(WhatChanged, self).__init__(context, outstream)

    self._workspace = workspace

    self._parent = context.options.what_changed_create_prefix
    self._show_files = context.options.what_changed_show_files

    self._filemap = defaultdict(set)

  def console_output(self, _):
    touched_files = self._get_touched_files()
    if self._show_files:
      for file_ in touched_files:
        yield file_
    else:
      touched_targets = set()
      for file_ in touched_files:
        for touched_target in self._owning_targets(file_):
          if touched_target not in touched_targets:
            touched_targets.add(touched_target)
            yield str(touched_target.address)

  def _get_touched_files(self):
    try:
      return self._workspace.touched_files(self._parent)
    except Workspace.WorkspaceError as e:
      raise TaskError(e)

  def _owning_targets(self, file_):
    for build_file in self._candidate_owners(file_):
      is_build_file = (build_file.full_path == os.path.join(get_buildroot(), file_))
      for address in Target.get_all_addresses(build_file):
        target = Target.get(address)

        # A synthesized target can never own permanent files on disk
        if target != target.derived_from:
          # TODO(John Sirois): tighten up the notion of targets written down in a BUILD by a user
          # vs. targets created by pants at runtime.
          continue

        if target and (is_build_file or ((has_sources(target) or has_resources(target))
                                         and self._owns(target, file_))):
          yield target

  def _candidate_owners(self, file_):
    build_file = BuildFile(get_buildroot(), relpath=os.path.dirname(file_), must_exist=False)
    if build_file.exists():
      yield build_file
    for sibling in build_file.siblings():
      yield sibling
    for ancestor in build_file.ancestors():
      yield ancestor

  def _owns(self, target, file_):
    if target not in self._filemap:
      files = self._filemap[target]
      files_owned_by_target = target.sources if has_sources(target) else []
      # TODO (tdesai): This case to handle resources in PythonTarget.
      # Remove this when we normalize resources handling across python and jvm targets.
      if has_resources(target):
        for resource in target.resources:
          if isinstance(resource, Compatibility.string):
            files_owned_by_target.extend(target.resources)
      for owned_file in files_owned_by_target:
        owned_path = os.path.join(target.target_base, owned_file)
        files.add(owned_path)
    return file_ in self._filemap[target]


class Workspace(AbstractClass):
  """Tracks the state of the current workspace."""

  class WorkspaceError(Exception):
    """Indicates a problem reading the local workspace."""

  @abstractmethod
  def touched_files(self, parent):
    """Returns the set of paths modified between the given parent commit and the current local
    workspace state.
    """


class ScmWorkspace(Workspace):
  """A workspace that uses an Scm to determine the touched files."""

  def __init__(self, scm):
    super(ScmWorkspace, self).__init__()

    self._scm = scm or get_scm()

  def touched_files(self, parent):
    try:
      return self._scm.changed_files(from_commit=parent, include_untracked=True)
    except Scm.ScmException as e:
      raise self.WorkspaceError("Problem detecting changed files.", e)


class ScmWhatChanged(WhatChanged):
  def __init__(self, context, scm=None, outstream=sys.stdout):
    """Creates a WhatChanged task that uses an Scm to determine changed files.

    context:    The pants execution context.
    scm:        The scm to use, taken from the globally configured scm if None.
    outstream:  The stream to write changed files or targets to.
    """
    super(ScmWhatChanged, self).__init__(context, ScmWorkspace(scm or get_scm()), outstream)
