# 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.

import os
import yaml
from vcstools.common import urlopen_netrc
from wstool.common import MultiProjectException

__REPOTYPES__ = ['svn', 'bzr', 'hg', 'git', 'tar']
__ALLTYPES__ = __REPOTYPES__ + ['other', 'setup-file']

## The Path spec is a leightweigt object to transport the
## specification of a config element between functions,
## independently of yaml structure.
## Specifications are persisted in yaml, this file deals
## with manipulations of any such structures representing configs as
## yaml.
## get_path_spec_from_yaml turns yaml into path_spec, and pathspec
## get_legacy_yaml returns yaml.


def get_yaml_from_uri(uri):
    """reads and parses yaml from a local file or remote uri"""
    stream = None
    try:
        try:
            if os.path.isfile(uri):
                try:
                    stream = open(uri, 'r')
                except IOError as ioe:
                    raise MultiProjectException(
                        "Unable open file [%s]: %s" % (uri, ioe))
            else:
                try:
                    stream = urlopen_netrc(uri)
                except IOError as ioe2:
                    raise MultiProjectException(
                        "Unable to download URL [%s]: %s" % (uri, ioe2))
        except ValueError as vae:
            raise MultiProjectException(
                "Is not a local file, nor a valid URL [%s] : %s" % (uri, vae))
        if not stream:
            raise MultiProjectException("couldn't load config uri %s" % uri)
        try:
            yamldata = yaml.load(stream)
        except yaml.YAMLError as yame:
            raise MultiProjectException(
                "Invalid multiproject yaml format in [%s]: %s" % (uri, yame))

        # we want a list or a dict, but pyyaml parses xml as string
        if type(yamldata) == 'str':
            raise MultiProjectException(
                "Invalid multiproject yaml format in [%s]: %s" % (uri, yamldata))
    finally:
        if stream is not None:
            stream.close()
    return yamldata


def get_path_specs_from_uri(uri, config_filename=None, as_is=False):
    """
    Builds a list of PathSpec elements from several types of input
    locations, "uris".
    The function treats other workspace folders/files as special uris
    to prevent mutual conflicts.

    :param uri: a folder, a file, or a web url
    :param config_filename: name for files to be treated special
    as other workspaces
    :param as_is: do not rewrite, used for loading the current
    workspace config without rewriting
    """
    if os.path.isdir(uri):
        if (config_filename is not None and
            os.path.isfile(os.path.join(uri, config_filename))):

            uri = os.path.join(uri, config_filename)
        else:
            # plain folders returned as themselves
            return [PathSpec(local_name=uri)]
    yaml_spec = get_yaml_from_uri(uri)
    if yaml_spec is None:
        return []
    specs = [get_path_spec_from_yaml(x) for x in yaml_spec]

    if (config_filename is not None and
        not as_is and
        os.path.isfile(uri) and
        os.path.basename(uri) == config_filename):

        # treat config files and folders with such files special
        # to prevent 2 workspaces from interacting
        specs = rewrite_included_source(specs, os.path.dirname(uri))
    return specs


def rewrite_included_source(source_path_specs, source_dir):
    """
    assumes source_path_specs is the contents of a config file in
    another directory source dir. It rewrites all elements, by changing
    any relative path relative to source dir and changing vcs
    types to non-vcs types types, to prevent two environments from
    conflicting
    """
    for index, pathspec in enumerate(source_path_specs):
        local_name = os.path.normpath(os.path.join(source_dir,
                                                   pathspec.get_local_name()))
        pathspec.set_local_name(local_name)
        if pathspec.get_path() is not None:
            path = os.path.normpath(
                os.path.join(source_dir, pathspec.get_path()))
            pathspec.set_path(path)
        pathspec.detach_vcs_info()
        source_path_specs[index] = pathspec
    return source_path_specs


def aggregate_from_uris(config_uris, config_filename=None, allow_other_element=True):
    """
    Builds a List of PathSpec from a list of location strings (uri,
    paths). If locations is a folder, attempts to find config_filename
    in it, and use "folder/config_filename" instead(rewriting element
    path and stripping scm nature), else add folder as PathSpec.
    Anything else, parse yaml at location, and add a PathSpec for each
    element.

    :param config_uris: source of yaml
    :param config_filename: file to use when given a folder
    :param allow_other_element: if False, discards elements
    to be added without SCM information
    """
    aggregate_source_yaml = []
    # build up a merged list of config elements from all given config_uris
    if config_uris is None:
        return []
    for loop_uri in config_uris:
        source_path_specs = get_path_specs_from_uri(
            loop_uri, config_filename)
        # allow duplicates, dealt with in Config class
        if not allow_other_element:
            for spec in source_path_specs:
                if not spec.get_scmtype():
                    raise MultiProjectException(
                        "Forbidden non-SCM element: %s (%s)" %
                        (spec.get_local_name(), spec.get_legacy_type()))
        aggregate_source_yaml.extend(source_path_specs)
    return aggregate_source_yaml


class PathSpec:
    def __init__(self,
                 # localname is used as ID, currently also is used as path
                 local_name,
                 scmtype=None,
                 uri=None,
                 version=None,
                 curr_version=None,
                 tags=None,
                 revision=None,
                 currevision=None,
                 remote_revision=None,
                 path=None,
                 curr_uri=None):
        """
        Fills in local properties based on dict, unifies different syntaxes
        :param local-name: to be unique within config, filesystem path to folder
        :param scmtype: one of __ALLTYPES__
        :param uri: uri from config file
        :param version: version label from config file (branchname, tagname, sha-id)
        :param cur_version: version information label(s) from VCS (branchname, remote, tracking branch)
        :param tags: arbirtrary meta-information (used for ROS package indexing)
        :param revision: unique id of label stored in version
        :param currrevision: unique id of actual version in file system
        :param path: path to folder (currently equivalent to local_name)
        :param curr_uri: actual remote uri used in local checkout
        """
        self._local_name = local_name
        self._path = path
        self._uri = uri
        self._curr_uri = curr_uri
        self._version = version
        self._curr_version = curr_version
        self._scmtype = scmtype
        self._tags = tags or []
        self._revision = revision
        self._currevision = currevision
        self._remote_revision = remote_revision

    def __str__(self):
        return str(self.get_legacy_yaml())

    def __repr__(self):
        return "PathSpec(%s)" % self.__str__()

    def __eq__(self, other):
        if isinstance(other, self.__class__):
            return self.__dict__ == other.__dict__
        else:
            return False

    def __ne__(self, other):
        return not self.__eq__(other)

    def detach_vcs_info(self):
        """if wrapper has VCS information, remove it to make it a plain folder"""
        if self._scmtype is not None:
            self._scmtype = None
            self._uri = None
            self._version = None
            self._curr_version = None
            self._revision = None
            self._currevision = None
            self._remote_revision = None

    def get_legacy_type(self):
        """return one of __ALLTYPES__"""
        if self._scmtype is not None:
            return self._scmtype
        elif self._tags is not None and 'setup-file' in self._tags:
            return 'setup-file'
        return 'other'

    def get_legacy_yaml(self):
        """
        return something like
        {hg: {local-name: common,
              version: common-1.0.2,
              uri: https://kforge.org/common/}}
        """
        # TODO switch to new syntax
        properties = {'local-name': self._local_name}
        if self._uri is not None:
            properties['uri'] = self._uri
        if self._version is not None:
            properties['version'] = self._version
        if self._tags is not None:
            for tag in self._tags:
                if tag != 'setup-file' and tag != []:
                    if type(tag) == dict:
                        properties.update(tag)
                    else:
                        properties[tag] = None
        yaml_dict = {self.get_legacy_type(): properties}
        return yaml_dict

    def get_local_name(self):
        return self._local_name

    def set_local_name(self, local_name):
        self._local_name = local_name

    def get_path(self):
        return self._path

    def set_path(self, path):
        self._path = path

    def get_tags(self):
        return self._tags

    def get_scmtype(self):
        return self._scmtype

    def get_version(self):
        return self._version

    def get_curr_version(self):
        return self._curr_version

    def get_revision(self):
        return self._revision

    def get_current_revision(self):
        return self._currevision

    def get_remote_revision(self):
        return self._remote_revision

    def get_uri(self):
        return self._uri

    def get_curr_uri(self):
        return self._curr_uri


def get_path_spec_from_yaml(yaml_dict):
    """
    Fills in local properties based on dict, unifies different syntaxes
    """
    local_name = None
    uri = None
    version = None
    scmtype = None
    tags = []
    if type(yaml_dict) != dict:
        raise MultiProjectException(
            "Yaml for each element must be in YAML dict form: %s " % yaml_dict)
    # old syntax:
# - hg: {local-name: common_rosdeps,
#        version: common_rosdeps-1.0.2,
#        uri: https://kforge.ros.org/common/rosdepcore}
# - setup-file: {local-name: /opt/ros/fuerte/setup.sh}
# - other: {local-name: /opt/ros/fuerte/share/ros}
# - other: {local-name: /opt/ros/fuerte/share}
# - other: {local-name: /opt/ros/fuerte/stacks}
    if yaml_dict is None or len(yaml_dict) == 0:
        raise MultiProjectException("no element in yaml dict.")
    if len(yaml_dict) > 1:
        raise MultiProjectException(
            "too many keys in element dict %s" % (list(yaml_dict.keys())))
    if not list(yaml_dict.keys())[0] in __ALLTYPES__:
        raise MultiProjectException(
            "Unknown element type '%s'" % (list(yaml_dict.keys())[0]))
    firstkey = list(yaml_dict.keys())[0]
    if firstkey in __REPOTYPES__:
        scmtype = list(yaml_dict.keys())[0]
    if firstkey == 'setup-file':
        tags.append('setup-file')
    values = yaml_dict[firstkey]
    if values is not None:
        for key, value in list(values.items()):
            if key == "local-name":
                local_name = value
            elif key == "meta":
                tags.append({key: value})
            elif key == "uri":
                uri = value
            elif key == "version":
                version = value
            else:
                raise MultiProjectException(
                    "Unknown key %s in %s" % (key, yaml_dict))
    # global validation
    if local_name is None:
        raise MultiProjectException(
            "Config element without a local-name: %s" % (yaml_dict))
    if scmtype != None:
        if uri is None:
            raise MultiProjectException(
                "scm type without declared uri in %s" % (values))
    # local_name is fixed, path may be normalized, made absolute, etc.
    path = local_name
    return PathSpec(local_name=local_name,
                    path=path,
                    scmtype=scmtype,
                    uri=uri,
                    version=version,
                    tags=tags)


def generate_config_yaml(config, filename, header, pretty=False,
                         sort_with_localname=False):
    """
    Writes file filename with header first and then the config as yaml
    """
    if not os.path.exists(config.get_base_path()):
        os.makedirs(config.get_base_path())
    config_filepath = os.path.realpath(os.path.join(config.get_base_path(), filename))
    with open(config_filepath, 'w+b') as f:
        if header is not None:
            f.write(header.encode('UTF-8'))
        if sort_with_localname:
            items = [x.get_legacy_yaml() for x in
                        sorted(config.get_source(),
                               key=lambda x:x.get_local_name())]
        else:
            items = [x.get_legacy_yaml() for x in config.get_source()]

        if not items:
            return

        if pretty:
            content = yaml.safe_dump(items, allow_unicode=True,
                                     default_flow_style=False)
        else:
            content = yaml.safe_dump(items)
        f.write(content.encode('UTF-8'))
