"""
Copyright 2021 Adobe
All Rights Reserved.

NOTICE: Adobe permits you to use, modify, and distribute this file in accordance
with the terms of the Adobe license agreement accompanying it.
"""

from __future__ import print_function

import glob
import logging
import os
import sys


LOGGER = logging.getLogger(__file__)


class VCSUnsupported(Exception):
    """Custom error signifying a specific VCS is not supported"""


def search_parent_dirs(directory, filename):
    """
    Search a directory and each parent directory for a filename.
    If found return the full path; otherwise return ''.
    """

    found = ''

    dir_parts = directory.split(os.path.sep)
    while dir_parts:
        filename_parts = dir_parts + [filename]
        filename_path = os.path.sep.join(filename_parts)
        if os.path.exists(filename_path):
            found = filename_path
            break
        dir_parts.pop()

    return found


def last_mtime(files, prefix=None):
    """
    Find the last modified time for a given list of files.
    """
    mtime = None

    if files:
        if prefix:
            _files = [os.path.join(prefix, f) for f in files]
        else:
            _files = files

        # NOTE: If the file doesn't exist then return 0, but then make sure a
        # 0 result gets mapped back to None.
        mtime = int(max([
            os.path.exists(fn) and os.stat(fn).st_mtime or 0 for fn in _files
        ])) or None

    return mtime


ST_MOD = 0
ST_ADD = 1
ST_REM = 2
ST_DEL = 3
ST_UNK = 4
ST_IGN = 5
ST_CLN = 6


class VCS:
    """Base class for a source tree managed by VCS"""

    def __init__(self):
        """Base constructor"""
        # Automatically set the VCS engine name
        self.vcs = self.__class__.__name__
        if self.vcs.startswith('VCS'):
            self.vcs = self.vcs[3:].lower()

        self.source_root = ''
        self._name = ''
        self._upstream_repo = ''

    @property
    def upstream_repo(self):
        """The location of the up-stream VCS repository."""
        return self._upstream_repo

    @property
    def name(self):
        """The name of the VCS project."""
        return self._name

    @property
    # pylint: disable=R0201
    def branch(self):
        """The branch name."""
        return None

    @property
    # pylint: disable=R0201,invalid-name
    def id(self):
        """The native commit identification."""
        return None

    @property
    # pylint: disable=R0201
    def user(self):
        """The user who performed the commit (if applicable)."""
        return 'n/a'

    @property
    def id_short(self):
        """
        Differs from .id for VCS that have large hash values that
        can be abbreviated.
        """
        return self.id

    @property
    def number(self):
        """
        This is the same as .id if commits have increasing commit numbers.
        Otherwise this simulates an increasing commit number.
        """
        return int(self.id)

    @property
    def modified(self):
        """
        Return none if there are no local modifications to the VCS checkout,
        otherwise return epoc seconds of the most recently modified file.

        NOTE: modified will update each time a file in the changeset is
        modified.  It cannot update, however, when a REMOVED file is added
        to the changeset because there may no longer be a file in the
        file system to stat() to get the removed time!  This means that
        modified can only updated with MODIFIED and ADDED files.
        """
        return last_mtime(
            sum(self.status()[ST_MOD:ST_ADD + 1], []),
            prefix=self.source_root
        ) or 0

    @property
    def release(self):
        """
        String representing the current state of the branch, including the
        commit number, id_short, and whether any modifications have been made
        to the local tree.
        """
        modified = self.modified
        modstr = f'.M{modified}'

        rel_str = f'{self.number}.I{self.id_short}{modstr}'

        return rel_str

    @property
    def id_string(self):
        """
        String providing a unique id of the branch and release.
        """
        return str(self.branch) + "-" + str(self.release)

    # pylint: disable=R0201
    def status(self):
        """
        Returns a tuple of lists of modified files in the changeset:
        (
            [MODIFIED, ...], # ST_MOD: changeset is modifying files
            [ADDED, ...],    # ST_ADD: changeset is adding file
            [REMOVED, ...],  # ST_REM: changeset is removing file
            [DELETED, ...],  # ST_DEL: file is missing but not in changeset
            [UNKNOWN, ...],  # ST_UNK: file exists but not in changeset
            [IGNORED, ...],  # ST_IGN: file exists but VCS explicitly ignores
            [CLEAN, ...],    # ST_CLN: file tracked by VCS and unmodified
        )
        """
        return [], [], [], [], [], [], []

    def list_files(self):
        """
        Return a list of files known to the VCS
        (committed + changeset(added, moved) - changeset(deleted))
        """
        raise NotImplementedError(f'VCS module {self.vcs} must implement list_files()')

    def info(self, include_files=False):
        """
        Return vcs information as a dict, optionally including full file
        information.
        """
        info = {
            'type': self.vcs,
            'upstream_repo': self.upstream_repo,
            'name': self.name,
            'branch': self.branch,
            'id': self.id,
            'id_short': self.id_short,
            'id_string': self.id_string,
            'number': self.number,
            'user': self.user,
            'release': self.release,
        }

        if include_files:
            info['files'] = self.list_files()
            # Do not report UNKNOWN or IGNORED files
            status_info = list(self.status())
            status_info[ST_UNK] = []
            status_info[ST_IGN] = []
            info['filestatus'] = status_info

        return info

    def __str__(self):
        """String represenation"""
        return f'{self.__class__.__name__}({self.source_root})'


def load_vcs(name, directory, *args, **argv):
    """
    Load a specific vcs module for the given directory and arguments.

    Args:
        name: the vcs module name
        directory: the directory to load the vcs module for
    Raises:
        VCSUnsupported: if the vcs module does not support the given directory
        TypeError: if the vcs module does not support the given directory
    """
    LOGGER.debug(f'Loading VCS module: {name}')
    vcs_module = __import__('.'.join((__name__, name)), fromlist=[__name__])
    vcs = vcs_module.VCS(directory, *args, **argv)
    return vcs


def detect_vcss(directory, *args, **argv):
    """
    Interrogate the given directory for vcs information, returning an object
    that can be used to obtain information about the current conditions of the
    source tree.
    """
    directory = directory or os.getcwd()
    vcs_dir = os.path.dirname(__file__)
    # Ignore things that don't look like python modules
    # and list archive.py last.
    vcs_files = glob.glob(os.path.join(vcs_dir, '*.py'))
    possible_vcs = []
    archive_vcs = None

    errors = []
    for modpath in vcs_files:
        modname, _ = os.path.splitext(os.path.basename(modpath))
        # skip things like '__init__.py'
        if modname.startswith('_'):
            continue

        try:
            vcs = load_vcs(modname, directory, *args, **argv)
            if hasattr(vcs, 'is_archive') and vcs.is_archive:
                # if an "archive" vcs config is present, it always wins
                return [archive_vcs]

            possible_vcs.append(vcs)
        except (VCSUnsupported, TypeError) as err:
            LOGGER.debug(f'Failed {modname}: {err}')
            errors.append(str(err))

    if not possible_vcs:
        # pylint: disable=C0301
        message = f'No recognized VCS management of source tree "{directory}" - do you need to login to a VCS?'
        for error in errors:
            message += f'\n\tWARNING: {error}'
        raise VCSUnsupported(message)

    return possible_vcs


def detect_vcs(directory, *args, **argv):
    """
    Detects the VCS type for a given directory.
    :param directory: the directory to determine the VCS for
    :return: the VCS type
    """
    possible_vcss = detect_vcss(directory, *args, **argv)
    LOGGER.debug(f'Possible VCSs: {possible_vcss}', possible_vcss)

    if len(possible_vcss) > 1:
        possible_vcss_str = ', '.join([vcs.vcs for vcs in possible_vcss])
        LOGGER.warning(f'WARNING: Multiple VCS matches: {possible_vcss_str}')

    return possible_vcss[0]


try:
    import setuptools.command.egg_info

    # pyline: disable=R0904
    class VCSInfoEggInfo(setuptools.command.egg_info.egg_info):
        """
        Override the egg_info command to appropriately set build tags.
        """

        command_name = 'egg_info'

        def tags(self):
            sourcedir = os.path.dirname(
                os.path.abspath(self.distribution.script_name)
            )

            # first, see if we can get vcs information.
            try:
                vcs = detect_vcs(sourcedir)
                tag_build = f'.{vcs.number}'
                if vcs.modified > 0:
                    tag_build = f'{tag_build}.{vcs.modified}'
                return tag_build
            except TypeError:
                pass
            except VCSUnsupported:
                pass

            # if not fall back to the default routine
            return setuptools.command.egg_info.egg_info.tags(self)


except ImportError:
    pass


# Local Variables:
# fill-column: 100
# End:
