# Software License Agreement (BSD License)
#
# Copyright (c) 2009, Willow Garage, Inc.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
#  * Redistributions of source code must retain the above copyright
#    notice, this list of conditions and the following disclaimer.
#  * Redistributions in binary form must reproduce the above
#    copyright notice, this list of conditions and the following
#    disclaimer in the documentation and/or other materials provided
#    with the distribution.
#  * Neither the name of Willow Garage, Inc. nor the names of its
#    contributors may be used to endorse or promote products derived
#    from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.

from __future__ import print_function
import os
from config_elements import AVCSConfigElement, OtherConfigElement, SetupConfigElement
from common import MultiProjectException, normabspath, realpath_relation, is_web_uri


class Config:
  """
  A config is a set of config elements, each of which defines a folder or file
  and possibly a VCS from which to update the folder.
  """
  
  def __init__(self, path_specs, install_path, config_filename = None, extended_types=None, merge_strategy = 'KillAppend'):
    """
    :param config_source_dict: A list (e.g. from yaml) describing the config, list of dict, each dict describing one element.
    :param config_filename: When given a folder, Config
    :param merge_strategy: how to deal with entries with equivalent path. See insert_element
    
    will look in folder for file of that name for more config source, str.
    """
    assert install_path is not None
    if path_specs is None:
      raise MultiProjectException("Passed empty source to create config")
    # All API operations must grant that elements in trees have unique local_name and paths
    # Also managed (VCS) entries must be disjunct (meaning one cannot be in a child folder of another managed one)
    # The idea is that managed entries can safely be concurrently modified
    self.trees = []
    self.base_path = os.path.abspath(install_path)

    self.config_filename = None
    if config_filename is not None:
      self.config_filename = os.path.basename(config_filename)
    # using a registry primarily for unit test design
    self.registry = {'svn': AVCSConfigElement,
                     'git': AVCSConfigElement,
                     'hg': AVCSConfigElement,
                     'bzr': AVCSConfigElement,
                     'tar': AVCSConfigElement}
    if extended_types is not None:
      self.registry = dict(list(self.registry.items()) + list(extended_types.items()))

    for path_spec in path_specs:
      action = self.add_path_spec(path_spec, merge_strategy)
      # Usual action in init should be 'Append', anything else is unusual
      if action == 'KillAppend':
        print("Replace existing entry %s by appending."%path_spec.get_local_name())
      elif action == 'MergeReplace':
        print("Replace existing entry %s"%path_spec.get_local_name())
      elif action == 'MergeKeep':
        print("Keep existing entry %s, discard later one"%path_spec.get_local_name())

  def __str__(self):
    return str([str(x) for x in self.trees])

  def add_path_spec(self, path_spec, merge_strategy = 'KillAppend'):
    """
   goes through path_specs and builds up self.trees.
   May recursively pull elements from remote sources.
   
   :param merge_strategy: see insert_element
   :param path_specs: PathSpec objects
   :returns: merge action taken, see insert_element
   """
    #compute the local_path for the config element
    local_path = normabspath(path_spec.get_local_name(), self.get_base_path())
    if path_spec.get_scmtype() != None:
      return self._insert_vcs_path_spec(path_spec, local_path, merge_strategy)
    else:
      if path_spec.get_tags() is not None and 'setup-file' in path_spec.get_tags():
        elem = SetupConfigElement(local_path, os.path.normpath(path_spec.get_local_name()), properties = path_spec.get_tags())
        return self.insert_element(elem, merge_strategy)
      else:
        # we dont want files or other Config folders in this Config
        if not (os.path.isfile(local_path)
                or
                (self.config_filename is not None
                 and os.path.isdir(local_path)
                 and os.path.exists(os.path.join(local_path, self.config_filename)))):
          local_name = os.path.normpath(path_spec.get_local_name())
          elem = OtherConfigElement(local_path,
                                    local_name,
                                    path_spec.get_uri(),
                                    path_spec.get_version(),
                                    properties = path_spec.get_tags())
          return self.insert_element(elem, merge_strategy)
        else:
          print("!!!!!Warning: Not recursing into other config folder %s"%local_path)


  def _insert_vcs_path_spec(self, path_spec, local_path, merge_strategy = 'KillAppend'):
    # Get the version and source_uri elements
    source_uri = path_spec.get_uri()
    
    if (source_uri is not None
        and not is_web_uri(source_uri)
        and not os.path.isabs(source_uri)):
      source_uri = os.path.normpath(os.path.join(self.get_base_path(), source_uri))
      print("Warning: Converted relative uri path %s to abspath %s"%(path_spec.get_uri(), source_uri))
    version = path_spec.get_version()
    try:
      local_name = os.path.normpath(path_spec.get_local_name())
      elem = self._create_vcs_config_element(path_spec.get_scmtype(),
                                             local_path,
                                             local_name,
                                             source_uri,
                                             version,
                                             properties = path_spec.get_tags())
      return self.insert_element(elem, merge_strategy)
    except LookupError as ex:
      raise MultiProjectException("Abstracted VCS Config failed. Exception: %s" % ex)

  def insert_element(self, new_config_elt, merge_strategy = 'KillAppend'):
    """
   Insert ConfigElement to self.trees, checking for duplicate local-name or path first.
   In case local_name matches, follow given strategy
   
   - KillAppend (default): remove old element, append new at the end
   - MergeReplace: remove first such old element, insert new at that position.
   - MergeKeep: Discard new element
   
   In case local path matches but local name does not, raise Exception
   
   :returns: the action performed None, 'Append', 'KillAppend', 'MergeReplace', 'MergeKeep'"""
    removals = []
    replaced = False
    for index, loop_elt in enumerate (self.trees):
      # if paths are os.path.realpath, no symlink problems.
      relationship = realpath_relation(loop_elt.get_path(), new_config_elt.get_path())
      if relationship == 'SAME_AS':
        if os.path.normpath(loop_elt.get_local_name()) != os.path.normpath(new_config_elt.get_local_name()):
          raise MultiProjectException("Elements with different local_name target the same path: %s, %s"%(loop_elt, new_config_elt))
        else:
          if (loop_elt == new_config_elt):
            return None
          if (merge_strategy == 'MergeReplace'
              or (merge_strategy == 'KillAppend' and index == len(self.trees)-1)):
            self.trees[index] = new_config_elt
            # keep looping to check for overlap when replacing non-scm with scm entry
            replaced = True
            if (loop_elt.is_vcs_element or not new_config_elt.is_vcs_element):
              return 'MergeReplace'
          elif merge_strategy == 'KillAppend':
            removals.append(loop_elt)
          elif merge_strategy == 'MergeKeep':
            return 'MergeKeep'
          else:
            raise LookupError("No such merge strategy: %s"%str(merge_strategy))
      elif ((relationship == 'CHILD_OF' and new_config_elt.is_vcs_element())
            or (relationship == 'PARENT_OF' and loop_elt.is_vcs_element())):
        # we do not allow any elements to be children of scm elements
        # to allow for parallel updates and because rosinstall may
        # delete scm folders on update, and thus subfolders can be
        # deleted with their parents
        raise MultiProjectException("Managed Element paths overlap: %s, %s"%(loop_elt, new_config_elt))
    if replaced:
      return 'MergeReplace'
    for loop_elt in removals:
      self.trees.remove(loop_elt)
    self.trees.append(new_config_elt)
    if len(removals) > 0:
      return 'KillAppend'
    return 'Append'

  def remove_element(self, local_name):
    """
    Removes element in the tree with the given local name (should be only one)
    
    :returns: True if such an element was found
    """
    removals = []
    for t in self.trees:
      if t.get_local_name() == local_name:
        removals.append(t)
    if len(removals) > 0:
      for t in removals:
        self.trees.remove(t)
      return True
    return False

  def _create_vcs_config_element(self, scmtype, path, local_name, uri, version = '', properties = None):
    try:
      eclass = self.registry[scmtype]
    except LookupError:
      raise MultiProjectException("No VCS client registered for vcs type %s"%scmtype)
    return eclass(scmtype = scmtype, path = path, local_name = local_name, uri = uri, version = version, properties = properties)

  def get_base_path(self):
    return self.base_path

  def get_config_filename(self):
    return self.config_filename
  

  def get_source(self):
    """
    :returns: all elements that got added by user keystrokes (CLI and changed .rosinstall)
    """
    source_aggregate = []
    for t in self.trees:
      source_aggregate.append(t.get_path_spec())
    return source_aggregate  

  
  def get_config_elements(self):
    source_aggregate = []
    for t in self.trees:
      source_aggregate.append(t)
    return source_aggregate

