"""Define the global type system that allows adding strong type information to python objects."""

# This file is adapted from python code released by WellDone International
# under the terms of the LGPLv3.  WellDone International's contact information is
# info@welldone.org
# http://welldone.org
#
# Modifications to this file from the original created at WellDone International
# are copyright Arch Systems Inc.

# typeinfo.py
# Basic routines for converting information from string or other binary
# formats to python types and for displaying those types in supported
# formats

import os.path
import imp
import logging
import sys
from typedargs.exceptions import ValidationError, ArgumentError, KeyValueException
from typedargs import types

class TypeSystem:
    """
    TypeSystem permits the inspection of defined types and supports
    converted string and binary values to and from these types.
    """

    def __init__(self, *args):
        """
        Create a TypeSystem by importing all of the types defined in modules passed
        as arguments to this function.  Each module is imported using
        """

        self.interactive = False
        self.known_types = {}
        self.type_factories = {}
        self.logger = logging.getLogger(__name__)

        for arg in args:
            self.load_type_module(arg)

        self._lazy_type_sources = []
        self.failed_sources = []

    def register_type_source(self, source, name=None):
        """Register an external source of types.

        This function does not actually load any external types, it just
        keeps track of the source, which must either be a str that is
        interpreted as a python entry_point group or a callable that
        will be passed an instance of this type system.

        External type sources will only be loaded in an as needed basis when
        a type is encountered (and needed) that is not currently known.  At
        that point external type sources will be considered until the type
        in question is found.

        If an external type source fails to load for some reason, it is logged
        but the error is not fatal.

        Args:
            source (str or callable): Either a pkg_resources entry_point
                group that will be searched for external types or a callable
                function that will be called as source(self) where self
                refers to this TypeSystem object.
            name (str): Optional short name to use for reporting errors having
                to do with this source of types.  This is most useful to pass
                when source is a callable.
        """

        self._lazy_type_sources.append((source, name))

    def convert_to_type(self, value, typename, **kwargs):
        """
        Convert value to type 'typename'

        If the conversion routine takes various kwargs to
        modify the conversion process, \\**kwargs is passed
        through to the underlying conversion function
        """
        try:
            if isinstance(value, bytearray):
                return self.convert_from_binary(value, typename, **kwargs)

            typeobj = self.get_type(typename)

            conv = typeobj.convert(value, **kwargs)
            return conv
        except (ValueError, TypeError) as exc:
            raise ValidationError("Could not convert value", type=typename, value=value, error_message=str(exc))

    def convert_from_binary(self, binvalue, type, **kwargs):
        """
        Convert binary data to type 'type'.

        'type' must have a convert_binary function.  If 'type'
        supports size checking, the size function is called to ensure
        that binvalue is the correct size for deserialization
        """

        size = self.get_type_size(type)
        if size > 0 and len(binvalue) != size:
            raise ArgumentError("Could not convert type from binary since the data was not the correct size", required_size=size, actual_size=len(binvalue), type=type)

        typeobj = self.get_type(type)

        if not hasattr(typeobj, 'convert_binary'):
            raise ArgumentError("Type does not support conversion from binary", type=type)

        return typeobj.convert_binary(binvalue, **kwargs)

    def get_type_size(self, type):
        """
        Get the size of this type for converting a hex string to the
        type. Return 0 if the size is not known.
        """

        typeobj = self.get_type(type)

        if hasattr(typeobj, 'size'):
            return typeobj.size()

        return 0

    def format_value(self, value, type, format=None, **kwargs):
        """
        Convert value to type and format it as a string

        type must be a known type in the type system and format,
        if given, must specify a valid formatting option for the
        specified type.
        """

        typed_val = self.convert_to_type(value, type, **kwargs)
        typeobj = self.get_type(type)

        #Allow types to specify default formatting functions as 'default_formatter'
        #otherwise if not format is specified, just convert the value to a string
        if format is None:
            if hasattr(typeobj, 'default_formatter'):
                format_func = getattr(typeobj, 'default_formatter')
                return format_func(typed_val, **kwargs)

            return str(typed_val)

        formatter = "format_%s" % str(format)
        if not hasattr(typeobj, formatter):
            raise ArgumentError("Unknown format for type", type=type, format=format, formatter_function=formatter)

        format_func = getattr(typeobj, formatter)
        return format_func(typed_val, **kwargs)

    @classmethod
    def _validate_type(cls, typeobj):
        """
        Validate that all required type methods are implemented.

        At minimum a type must have:
        - a convert() or convert_binary() function
        - a default_formatter() function

        Raises an ArgumentError if the type is not valid
        """

        if not (hasattr(typeobj, "convert") or hasattr(typeobj, "convert_binary")):
            raise ArgumentError("type is invalid, does not have convert or convert_binary function", type=typeobj, methods=dir(typeobj))

        if not hasattr(typeobj, "default_formatter"):
            raise ArgumentError("type is invalid, does not have default_formatter function", type=typeobj, methods=dir(typeobj))

    def is_known_type(self, type_name):
        """Check if type is known to the type system.

        Returns:
            bool: True if the type is a known instantiated simple type, False otherwise
        """

        type_name = str(type_name)
        if type_name in self.known_types:
            return True

        return False

    def split_type(self, typename):
        """
        Given a potentially complex type, split it into its base type and specializers
        """

        name = self._canonicalize_type(typename)
        if '(' not in name:
            return name, False, []

        base, sub = name.split('(')
        if len(sub) == 0 or sub[-1] != ')':
            raise ArgumentError("syntax error in complex type, no matching ) found", passed_type=typename, basetype=base, subtype_string=sub)

        sub = sub[:-1]

        subs = sub.split(',')
        return base, True, subs

    def instantiate_type(self, typename, base, subtypes):
        """Instantiate a complex type."""

        if base not in self.type_factories:
            raise ArgumentError("unknown complex base type specified", passed_type=typename, base_type=base)

        base_type = self.type_factories[base]

        #Make sure all of the subtypes are valid
        for sub_type in subtypes:
            try:
                self.get_type(sub_type)
            except KeyValueException as exc:
                raise ArgumentError("could not instantiate subtype for complex type", passed_type=typename, sub_type=sub_type, error=exc)

        typeobj = base_type.Build(*subtypes, type_system=self)
        self.inject_type(typename, typeobj)

    @classmethod
    def _canonicalize_type(cls, typename):
        return typename.replace(' ', '')

    @classmethod
    def _is_factory(cls, typeobj):
        """
        Determine if typeobj is a factory for producing complex types
        """

        if hasattr(typeobj, 'Build'):
            return True

        return False

    def get_type(self, type_name):
        """Return the type object corresponding to a type name.

        If type_name is not found, this triggers the loading of
        external types until a matching type is found or until there
        are no more external type sources.
        """

        type_name = self._canonicalize_type(type_name)

        # Add basic transformations on common abbreviations
        if str(type_name) == 'int':
            type_name = 'integer'
        elif str(type_name) == 'str':
            type_name = 'string'
        elif str(type_name) == 'dict':
            type_name = 'basic_dict'

        if self.is_known_type(type_name):
            return self.known_types[type_name]

        base_type, is_complex, subtypes = self.split_type(type_name)
        if is_complex and base_type in self.type_factories:
            self.instantiate_type(type_name, base_type, subtypes)
            return self.known_types[type_name]

        # If we're here, this is a type that we don't know anything about, so go find it.
        i = 0
        for i, (source, name) in enumerate(self._lazy_type_sources):
            if isinstance(source, str):
                import pkg_resources

                for entry in pkg_resources.iter_entry_points(source):
                    try:
                        mod = entry.load()
                        type_system.load_type_module(mod)
                    except:  #pylint:disable=W0702; We want to catch everything here since we don't want external plugins breaking us
                        fail_info = ("Entry point group: %s, name: %s" % (source, entry.name), sys.exc_info)
                        logging.exception("Error loading external type source from entry point, group: %s, name: %s", source, entry.name)
                        self.failed_sources.append(fail_info)
            else:
                try:
                    source(self)
                except:  #pylint:disable=W0702; We want to catch everything here since we don't want external plugins breaking us
                    fail_info = ("source: %s" % name, sys.exc_info)
                    logging.exception("Error loading external type source, source: %s", source)
                    self.failed_sources.append(fail_info)

            # Only load as many external sources as we need to resolve this type_name
            if self.is_known_type(type_name) or (is_complex and base_type in self.type_factories):
                break

        self._lazy_type_sources = self._lazy_type_sources[i:]

        # If we've loaded everything and we still can't find it then there's a configuration error somewhere
        if not (self.is_known_type(type_name) or (is_complex and base_type in self.type_factories)):
            raise ArgumentError("get_type called on unknown type", type=type_name, failed_external_sources=[x[0] for x in self.failed_sources])

        return self.get_type(type_name)

    def is_known_format(self, type, format):
        """
        Check if format is known for given type.

        Returns boolean indicating if format is valid for the specified type.
        """

        typeobj = self.get_type(type)

        formatter = "format_%s" % str(format)
        if not hasattr(typeobj, formatter):
            return False

        return True

    def inject_type(self, name, typeobj):
        """
        Given a module-like object that defines a type, add it to our type system so that
        it can be used with the iotile tool and with other annotated API functions.
        """

        name = self._canonicalize_type(name)
        _, is_complex, _ = self.split_type(name)

        if self.is_known_type(name):
            raise ArgumentError("attempting to inject a type that is already defined", type=name)

        if (not is_complex) and self._is_factory(typeobj):
            if name in self.type_factories:
                raise ArgumentError("attempted to inject a complex type factory that is already defined", type=name)
            self.type_factories[name] = typeobj
        else:
            self._validate_type(typeobj)
            self.known_types[name] = typeobj

        if not hasattr(typeobj, "default_formatter"):
            raise ArgumentError("type is invalid, does not have default_formatter function", type=typeobj, methods=dir(typeobj))

    def load_type_module(self, module):
        """
        Given a module that contains a list of some types find all symbols in the module that
        do not start with _ and attempt to import them as types.
        """

        for name in (x for x in dir(module) if not x.startswith('_')):
            typeobj = getattr(module, name)

            try:
                self.inject_type(name, typeobj)
            except ArgumentError:
                pass

    def load_external_types(self, path):
        """
        Given a path to a python package or module, load that module, search for all defined variables
        inside of it that do not start with _ or __ and inject them into the type system.  If any of the
        types cannot be injected, silently ignore them unless verbose is True.  If path points to a module
        it should not contain the trailing .py since this is added automatically by the python import system
        """

        folder, filename = os.path.split(path)

        try:
            fileobj, pathname, description = imp.find_module(filename, [folder])
            mod = imp.load_module(filename, fileobj, pathname, description)
        except ImportError as exc:
            raise ArgumentError("could not import module in order to load external types", module_path=path, parent_directory=folder, module_name=filename, error=str(exc))

        self.load_type_module(mod)

def iprint(stringable):
    """
    A simple function to only print text if in an interactive session.
    """

    if type_system.interactive:
        print(stringable)

#In order to support function annotations that must be resolved to types when modules
#are imported, create a default TypeSystem object that is used globally to store type
#information

type_system = TypeSystem(types)  # pylint: disable=invalid-name


"""The basic class that is used to store metadata about a function."""

import inspect
import sys
from typedargs import typeinfo
from .exceptions import TypeSystemError, ArgumentError, ValidationError, InternalError
from .basic_structures import ParameterInfo, ReturnInfo
from .doc_annotate import parse_docstring

class AnnotatedMetadata: #pylint: disable=R0902; These instance variables are required.
    """All of the associated metadata for an annotated function or class.

    Args:
        func (callable): The function that we are annotated so that
            we can pull out relevant argument spec information.  If func
            is a class, we pull arguments from its __init__ routine
        name (str): An optional name that will overwrite the default name
            of this function
    """

    def __init__(self, func, name=None):
        self.annotated_params = {}
        self._has_self = False

        docstring = func.__doc__

        if inspect.isclass(func):
            # If we're annotating a class, the name of the class should be
            # the class name so keep track of that before looking at its
            # __init__ function.
            if name is None:
                name = func.__name__

            func = func.__init__

            # If __init__ has anotated params, copy them to the class so
            # we print correct signatures
            if hasattr(func, 'metadata'):
                self.annotated_params = func.metadata.annotated_params

        # If we are called to annotate a context, we won't necessarily
        # have any arguments
        try:
            if sys.version_info.major >= 3:
                spec = inspect.getfullargspec(func)
                args, varargs, kwargs, defaults = spec[:4]
            else:
                args, varargs, kwargs, defaults = inspect.getargspec(func)  # pylint: disable=deprecated-method

            # Skip self argument if this is a method function
            if len(args) > 0 and args[0] == 'self':
                args = args[1:]
                self._has_self = True

            if defaults is None:
                defaults = []

            self.varargs = varargs
            self.kwargs = kwargs
            self.arg_names = args
            self.arg_defaults = defaults
        except TypeError:
            self.varargs = None
            self.kwargs = None
            self.arg_names = []
            self.arg_defaults = []

        self.return_info = ReturnInfo(None, None, False, None)

        if name is None:
            name = func.__name__

        self.name = name

        self.load_from_doc = False
        self._doc_parsed = False
        self._docstring = docstring

    def _ensure_loaded(self):
        if self.load_from_doc and not self._doc_parsed:
            params, ret_info = parse_docstring(self._docstring)
            for param_name, param_info in params.items():
                self.add_param(param_name, param_info.type_name, param_info.validators)

            if ret_info is not None:
                self.return_info = ret_info

            self._doc_parsed = True

    def spec_filled(self, pos_args, kw_args):
        """Check if we have enough arguments to call this function.

        Args:
            pos_args (list): A list of all the positional values we have.
            kw_args (dict): A dict of all of the keyword args we have.

        Returns:
            bool: True if we have a filled spec, False otherwise.
        """

        req_names = self.arg_names
        if len(self.arg_defaults) > 0:
            req_names = req_names[:-len(self.arg_defaults)]

        req = [x for x in req_names if x not in kw_args]
        return len(req) <= len(pos_args)

    def add_param(self, name, type_name, validators, desc=None):
        """Add type information for a parameter by name.

        Args:
            name (str): The name of the parameter we wish to annotate
            type_name (str): The name of the parameter's type
            validators (list): A list of either strings or n tuples that each
                specify a validator defined for type_name.  If a string is passed,
                the validator is invoked with no extra arguments.  If a tuple is
                passed, the validator will be invoked with the extra arguments.
            desc (str): Optional parameter description.
        """

        if name in self.annotated_params:
            raise TypeSystemError("Annotation specified multiple times for the same parameter", param=name)

        if name not in self.arg_names and name != self.varargs and name != self.kwargs:
            raise TypeSystemError("Annotation specified for unknown parameter", param=name)

        info = ParameterInfo(type_name, validators, desc)
        self.annotated_params[name] = info

    def typed_returnvalue(self, type_name, formatter=None):
        """Add type information to the return value of this function.

        Args:
            type_name (str): The name of the type of the return value.
            formatter (str): An optional name of a formatting function specified
                for the type given in type_name.
        """
        self.return_info = ReturnInfo(type_name, formatter, True, None)

    def string_returnvalue(self):
        """Mark the return value as data that should be converted with str."""
        self.return_info = ReturnInfo(None, str, True, None)

    def custom_returnvalue(self, printer, desc=None):
        """Use a custom function to print the return value.

        Args:
            printer (callable): A function that should take in the return
                value and convert it to a string.
            desc (str): An optional description of the return value.
        """
        self.return_info = ReturnInfo(None, printer, True, desc)

    def has_varargs(self):
        """Check if this function supports variable arguments."""
        return self.varargs is not None

    def has_kwargs(self):
        """Check if this function supports arbitrary keyword arguments."""
        return self.kwargs is not None

    def returns_data(self):
        """Check if this function returns data."""

        self._ensure_loaded()

        return self.return_info.is_data

    def match_shortname(self, name, filled_args=None):
        """Try to convert a prefix into a parameter name.

        If the result could be ambiguous or there is no matching
        parameter, throw an ArgumentError

        Args:
            name (str): A prefix for a parameter name
            filled_args (list): A list of filled positional arguments that will be
                removed from consideration.

        Returns:
            str: The full matching parameter name
        """

        filled_count = 0
        if filled_args is not None:
            filled_count = len(filled_args)

        possible = [x for x in self.arg_names[filled_count:] if x.startswith(name)]
        if len(possible) == 0:
            raise ArgumentError("Could not convert short-name full parameter name, none could be found", short_name=name, parameters=self.arg_names)
        elif len(possible) > 1:
            raise ArgumentError("Short-name is ambiguous, could match multiple keyword parameters", short_name=name, possible_matches=possible)

        return possible[0]

    def param_type(self, name):
        """Get the parameter type information by name.

        Args:
            name (str): The full name of a parameter.

        Returns:
            str: The type name or None if no type information is given.
        """

        self._ensure_loaded()

        if name not in self.annotated_params:
            return None

        return self.annotated_params[name].type_name

    def signature(self, name=None):
        """Return our function signature as a string.

        By default this function uses the annotated name of the function
        however if you need to override that with a custom name you can
        pass name=<custom name>

        Args:
            name (str): Optional name to override the default name given
                in the function signature.

        Returns:
            str: The formatted function signature
        """

        self._ensure_loaded()

        if name is None:
            name = self.name

        num_args = len(self.arg_names)

        num_def = 0
        if self.arg_defaults is not None:
            num_def = len(self.arg_defaults)

        num_no_def = num_args - num_def

        args = []
        for i in range(0, len(self.arg_names)):
            typestr = ""

            if self.arg_names[i] in self.annotated_params:
                typestr = "{} ".format(self.annotated_params[self.arg_names[i]].type_name)

            if i >= num_no_def:
                default = str(self.arg_defaults[i-num_no_def])
                if len(default) == 0:
                    default = "''"

                args.append("{}{}={}".format(typestr, str(self.arg_names[i]), default))
            else:
                args.append(typestr + str(self.arg_names[i]))

        return "{}({})".format(name, ", ".join(args))

    def format_returnvalue(self, value):
        """Format the return value of this function as a string.

        Args:
            value (object): The return value that we are supposed to format.

        Returns:
            str: The formatted return value, or None if this function indicates
                that it does not return data
        """

        self._ensure_loaded()

        if not self.return_info.is_data:
            return None

        # If the return value is typed, use the type_system to format it
        if self.return_info.type_name is not None:
            return typeinfo.type_system.format_value(value, self.return_info.type_name, self.return_info.formatter)

        # Otherwise we expect a callable function to convert this value to a string
        return self.return_info.formatter(value)

    def convert_positional_argument(self, index, arg_value):
        """Convert and validate a positional argument.

        Args:
            index (int): The positional index of the argument
            arg_value (object): The value to convert and validate

        Returns:
            object: The converted value.
        """

        # For bound methods, skip self
        if self._has_self:
            if index == 0:
                return arg_value

            index -= 1

        arg_name = self.arg_names[index]
        return self.convert_argument(arg_name, arg_value)

    def check_spec(self, pos_args, kwargs=None):
        """Check if there are any missing or duplicate arguments.

        Args:
            pos_args (list): A list of arguments that will be passed as positional
                arguments.
            kwargs (dict): A dictionary of the keyword arguments that will be passed.

        Returns:
            dict: A dictionary of argument name to argument value, pulled from either
                the value passed or the default value if no argument is passed.

        Raises:
            ArgumentError: If a positional or keyword argument does not fit in the spec.
            ValidationError: If an argument is passed twice.
        """

        if kwargs is None:
            kwargs = {}

        if self.varargs is not None or self.kwargs is not None:
            raise InternalError("check_spec cannot be called on a function that takes *args or **kwargs")

        missing = object()

        arg_vals = [missing]*len(self.arg_names)
        kw_indices = {name: i for i, name in enumerate(self.arg_names)}

        for i, arg in enumerate(pos_args):
            if i >= len(arg_vals):
                raise ArgumentError("Too many positional arguments, first excessive argument=%s" % str(arg))

            arg_vals[i] = arg

        for arg, val in kwargs.items():
            index = kw_indices.get(arg)
            if index is None:
                raise ArgumentError("Cannot find argument by name: %s" % arg)

            if arg_vals[index] is not missing:
                raise ValidationError("Argument %s passed twice" % arg)

            arg_vals[index] = val

        # Fill in any default variables if their args are missing
        if len(self.arg_defaults) > 0:
            for i in range(0, len(self.arg_defaults)):
                neg_index = -len(self.arg_defaults) + i
                if arg_vals[neg_index] is missing:
                    arg_vals[neg_index] = self.arg_defaults[i]

        # Now make sure there isn't a missing gap
        if missing in arg_vals:
            index = arg_vals.index(missing)
            raise ArgumentError("Missing a required argument (position: %d, name: %s)" % (index, self.arg_names[index]))

        return {name: val for name, val in zip(self.arg_names, arg_vals)}

    def convert_argument(self, arg_name, arg_value):
        """Given a parameter with type information, convert and validate it.

        Args:
            arg_name (str): The name of the argument to convert and validate
            arg_value (object): The value to convert and validate

        Returns:
            object: The converted value.
        """

        self._ensure_loaded()

        type_name = self.param_type(arg_name)
        if type_name is None:
            return arg_value

        val = typeinfo.type_system.convert_to_type(arg_value, type_name)

        validators = self.annotated_params[arg_name].validators
        if len(validators) == 0:
            return val

        type_obj = typeinfo.type_system.get_type(type_name)

        # Run all of the validators that were defined for this argument.
        # If the validation fails, they will raise an exception that we convert to
        # an instance of ValidationError
        try:
            for validator_name, extra_args in validators:
                if not hasattr(type_obj, validator_name):
                    raise ValidationError("Could not find validator specified for argument", argument=arg_name, validator_name=validator_name, type=str(type_obj), method=dir(type_obj))

                validator = getattr(type_obj, validator_name)
                validator(val, *extra_args)
        except (ValueError, TypeError) as exc:
            raise ValidationError(exc.args[0], argument=arg_name, arg_value=val)

        return val


"""A generic key-value based exception class."""

# This file is adapted from python code released by WellDone International
# under the terms of the LGPLv3.  WellDone International's contact information is
# info@welldone.org
# http://welldone.org
#
# Modifications to this file from the original created at WellDone International
# are copyright Arch Systems Inc.

class KeyValueException(Exception):
    """An exception taking key value pairs containing exception information.

    KeyValueExceptions provide a convenient way to include additional information
    about the source or context of an exception without having to convert it all to
    a formatted string.

    Args:
        msg (string): A short message about the cause of this exception, not including
            its type or any placeholders
        **kwargs: Any named parameters that you would like included with this exception
    """

    def __init__(self, msg, **kwargs):
        self.params = kwargs
        self.msg = msg

        super(KeyValueException, self).__init__()

    def format(self, exclude_class=False):
        """Format this exception as a string including class name.

        Args:
            exclude_class (bool): Whether to exclude the exception class
                name when formatting this exception

        Returns:
            string: a multiline string with the message, class name and
                key value parameters passed to create the exception.
        """

        if exclude_class:
            msg = self.msg
        else:
            msg = "%s: %s" % (self.__class__.__name__, self.msg)

        if len(self.params) != 0:
            paramstring = "\n".join([str(key) + ": " + str(val) for key, val in self.params.items()])
            msg += "\nAdditional Information:\n" + paramstring

        return msg

    def format_msg(self):
        """Format this exception as a string excluding class name.

        Returns:
            string: a multiline string with the message and
                key value parameters passed to create the exception.
        """

        return self.format(exclude_class=True)

    def to_dict(self):
        """Convert this exception to a dictionary.

        Returns:
            dist: A dictionary of information about this exception,
                Has a 'reason' key, a 'type' key  and a dictionary of params
        """

        out = {}
        out['reason'] = self.msg
        out['type'] = self.__class__.__name__
        out['params'] = self.params

        return out

    def __str__(self):
        return self.format()

class ArgumentError(KeyValueException):
    """There is a problem with one of the arguments to the function.

    The method could not be called with the arguments passed.  This
    differs from InternalError in that when ArgumentError is returned,
    it is known that these arguments can never work for this function.

    An example would be passing three arguments to a function requiring
    4 arguments.
    """

    pass

class ValidationError(KeyValueException):
    """The type validators attached to a parameter failed.

    API routines can impose validation criteria on` their arguments in
    addition to requiring simply a certain type of argument.  A clasic
    example is the "path" type which can have validators like "readable"
    or "exists".  When validation fails, this error is thrown.
    """

    pass

class ConversionError(KeyValueException):
    """An error occurred converted an argument to its specified type.

    All API functions take typed parameters.  Each type defines conversion
    operators for python types that are logically related to it.  When no
    valid conversion exists for the data type passed, this error is thrown.
    """

    pass

class NotFoundError(KeyValueException):
    """A method was invoked by name that does not exist.

    Thrown when an attempt to execute an API method is made and the API
    method does not exist.
    """

    pass

class TypeSystemError(KeyValueException):
    """An unspecified internal error occurred in the typedargs type system.

    There was an error with the type system.  This can be due to improperly
    specifying an unknown type or because the required type was not properly loaded
    from an external module before a function that used that type was needed.
    """

    pass

class InternalError(KeyValueException):
    """An unexpected internal error prevented method completion.

    The method could not be completed with the user input passed for
    an unexpected reason.  This does not signify a bug in the API
    method code.  More details should be passed in the arguments.
    """

    pass


"""Utility functions that are only used internally inside typedargs."""

from .exceptions import ValidationError
from .metadata import AnnotatedMetadata

class BasicContext(dict):
    """A Basic context for holding functions in a Hierarchical Shell."""
    pass

def _check_and_execute(func, *args, **kwargs):
    """
    Check the type of all parameters with type information, converting
    as appropriate and then execute the function.
    """

    convargs = []

    #Convert and validate all arguments
    for i, arg in enumerate(args):
        val = func.metadata.convert_positional_argument(i, arg)
        convargs.append(val)

    convkw = {}
    for key, val in kwargs:
        convkw[key] = func.metadata.convert_argument(key, val)

    if not func.metadata.spec_filled(convargs, convkw):
        raise ValidationError("Not enough parameters specified to call function", function=func.metadata.name, signature=func.metadata.signature())

    retval = func(*convargs, **convkw)
    return retval

def _parse_validators(valids):
    """Parse a list of validator names or n-tuples, checking for errors.

    Returns:
        list((func_name, [args...])): A list of validator function names and a
            potentially empty list of optional parameters for each function.
    """

    outvals = []

    for val in valids:
        if isinstance(val, str):
            args = []
        elif len(val) > 1:
            args = val[1:]
            val = val[0]
        else:
            raise ValidationError("You must pass either an n-tuple or a string to define a validator", validator=val)

        name = "validate_%s" % str(val)
        outvals.append((name, args))

    return outvals

def context_name(con):
    """Given a context, return its proper name as a string."""
    if hasattr(con, 'metadata'):
        return con.metadata.name

    return str(con)

def find_all(container):
    """Find all annotated function inside of a container.

    Annotated functions are identified as those that:
    - do not start with a _ character
    - are either annotated with metadata
    - or strings that point to lazily loaded modules

    Args:
        container (object): The container to search for annotated functions.

    Returns:
        dict: A dict with all of the found functions in it.
    """

    if isinstance(container, dict):
        names = container.keys()
    else:
        names = dir(container)

    built_context = BasicContext()

    for name in names:
        # Ignore _ and __ names
        if name.startswith('_'):
            continue

        if isinstance(container, dict):
            obj = container[name]
        else:
            obj = getattr(container, name)

        # Check if this is an annotated object that should be included.  Check the type of
        # annotated to avoid issues with module imports where someone did from annotate import *
        # into the module causing an annotated symbol to be defined as a decorator

        # If we are in a dict context then strings point to lazily loaded modules so include them too.
        if isinstance(container, dict) and isinstance(obj, str):
            built_context[name] = obj
        elif hasattr(obj, 'metadata') and isinstance(getattr(obj, 'metadata'), AnnotatedMetadata):
            built_context[name] = obj

    return built_context


"""Decorators that allow you to annotate type information with a function or class."""

# This file is adapted from python code released by WellDone International
# under the terms of the LGPLv3.  WellDone International's contact information is
# info@welldone.org
# http://welldone.org
#
# Modifications to this file from the original created at WellDone International
# are copyright Arch Systems Inc.

import inspect
from decorator import decorate
from typedargs.exceptions import ArgumentError
from typedargs.utils import find_all, _check_and_execute, _parse_validators, context_name
from typedargs.metadata import AnnotatedMetadata
from typedargs.typeinfo import type_system  #pylint: disable=W0611; this is needed for backward compatibility

def context_from_module(module):
    """
    Given a module, create a context from all of the top level annotated
    symbols in that module.
    """

    con = find_all(module)

    if hasattr(module, "__doc__"):
        setattr(con, "__doc__", module.__doc__)

    name = module.__name__
    if hasattr(module, "_name_"):
        name = module._name_  # pylint: disable=W0212

    con = annotated(con, name)
    setattr(con, 'context', True)

    return name, con

def get_help(func):
    """Return usage information about a context or function.

    For contexts, just return the context name and its docstring
    For functions, return the function signature as well as its
    argument types.

    Args:
        func (callable): An annotated callable function

    Returns:
        str: The formatted help text
    """

    help_text = ""
    if isinstance(func, dict):
        name = context_name(func)

        help_text = "\n" + name + "\n\n"
        doc = inspect.getdoc(func)
        if doc is not None:
            doc = inspect.cleandoc(doc)
            help_text += doc + '\n'

        return help_text

    sig = func.metadata.signature()
    doc = inspect.getdoc(func)
    if doc is not None:
        doc = inspect.cleandoc(doc)

    help_text += "\n" + sig + "\n\n"
    if doc is not None:
        help_text += doc + '\n'

    if inspect.isclass(func):
        func = func.__init__

    # If we derived the parameter annotations from a docstring,
    # don't insert a custom arguments section since it already
    # exists.
    if func.metadata.load_from_doc:
        return help_text

    help_text += "\nArguments:\n"
    for key, info in func.metadata.annotated_params.items():
        type_name = info.type_name
        desc = ""
        if info.desc is not None:
            desc = info.desc

        help_text += "  - %s (%s): %s\n" % (key, type_name, desc)

    return help_text

# Decorators

def param(name, type_name, *validators, **kwargs):
    """Decorate a function to give type information about its parameters.

    This function stores a type name, optional description and optional list
    of validation functions along with the decorated function it is called
    on in order to allow run time type conversions and validation.

    Args:
        name (string): name of the parameter
        type_name (string): The name of a type that will be known to the type
            system by the time this function is called for the first time.  Types
            are lazily loaded so it is not required that the type resolve correctly
            at the point in the module where this function is defined.
        validators (list(string or tuple)): A list of validators.  Each validator
            can be defined either using a string name or as an n-tuple of the form
            [name, \\*extra_args].  The name is used to look up a validator function
            of the form validate_name, which is called on the parameters value to
            determine if it is valid.  If extra_args are given, they are passed
            as extra arguments to the validator function, which is called as:

            validator(value, \\*extra_args)
        desc (string): An optional descriptioon for this parameter that must be
            passed as a keyword argument.

    Returns:
        callable: A decorated function with additional type metadata
    """

    def _param(func):
        func = annotated(func)

        valids = _parse_validators(validators)
        func.metadata.add_param(name, type_name, valids, **kwargs)

        # Only decorate the function once even if we have multiple param decorators
        if func.decorated:
            return func

        func.decorated = True
        return decorate(func, _check_and_execute)

    return _param

def returns(desc=None, printer=None, data=True):
    """Specify how the return value of this function should be handled.

    Args:
        desc (str): A deprecated description of the return value
        printer (callable): A callable function that can format this return value
        data (bool): A deprecated parameter for specifying that this function
            returns data.
    """

    if data is False:
        raise ArgumentError("Specifying non data return type in returns is no longer supported")

    def _returns(func):
        annotated(func)
        func.custom_returnvalue(printer, desc)
        return func

    return _returns

def stringable(func):
    """Specify that the return value should just be printed as a string.

    Args:
        func (callable): The function that we wish to annotate.
    """

    func = annotated(func)
    func.metadata.string_returnvalue()
    return func

def return_type(type_name, formatter=None):
    """Specify that this function returns a typed value.

    Args:
        type_name (str): A type name known to the global typedargs type system
        formatter (str): An optional name of a formatting function specified
            for the type given in type_name.
    """

    def _returns(func):
        annotated(func)
        func.metadata.typed_returnvalue(type_name, formatter)
        return func

    return _returns

def context(name=None):
    """Declare that a class defines a context.

    Contexts are for use with HierarchicalShell for discovering
    and using functionality from the command line.

    Args:
        name (str): Optional name for this context if you don't want
            to just use the class name.
    """

    def _context(cls):
        annotated(cls, name)
        cls.context = True

        return cls

    return _context

def finalizer(func):
    """
    Indicate that this function destroys the context in which it is invoked, such as a quit method
    on a subprocess or a delete method on an object.
    """

    func = annotated(func)
    func.finalizer = True
    return func

def takes_cmdline(func):
    """Annotate that a function should take the entire command line."""
    func = annotated(func)
    func.takes_cmdline = True

    return func

def docannotate(func):
    """Annotate a function using information from its docstring.

    The annotation actually happens at the time the function is first called
    to improve startup time.  For this function to work, the docstring must be
    formatted correctly.  You should use the typedargs pylint plugin to make
    sure there are no errors in the docstring.
    """

    func = annotated(func)
    func.metadata.load_from_doc = True

    if func.decorated:
        return func

    func.decorated = True
    return decorate(func, _check_and_execute)

def annotated(func, name=None):
    """Mark a function as callable from the command line.

    This function is meant to be called as decorator.  This function
    also initializes metadata about the function's arguments that is
    built up by the param decorator.

    Args:
        func (callable): The function that we wish to mark as callable
            from the command line.
        name (str): Optional string that will override the function's
            built-in name.
    """

    if hasattr(func, 'metadata'):
        if name is not None:
            func.metadata = AnnotatedMetadata(func, name)
        return func

    func.metadata = AnnotatedMetadata(func, name)

    func.finalizer = False
    func.takes_cmdline = False
    func.decorated = False
    func.context = False

    return func

def short_description(func):
    """
    Given an object with a docstring, return the first line of the docstring
    """

    doc = inspect.getdoc(func)
    if doc is not None:
        doc = inspect.cleandoc(doc)
        lines = doc.splitlines()
        return lines[0]

    return ""


from functools import wraps
import collections
import crontab
import django
from django.core.management import get_commands, load_command_class

try:
    from importlib import import_module
except ImportError:
    from django.utils.importlib import import_module

from kronos.settings import PROJECT_MODULE, KRONOS_PYTHON, KRONOS_MANAGE, \
    KRONOS_PYTHONPATH, KRONOS_POSTFIX, KRONOS_PREFIX, KRONOS_BREADCRUMB
from django.conf import settings
from kronos.version import __version__
import six
from django.utils.module_loading import autodiscover_modules

Task = collections.namedtuple('Task', ['name', 'schedule', 'command', 'function'])

registry = set()

def load():
    """
    Load ``cron`` modules for applications listed in ``INSTALLED_APPS``.
    """
    autodiscover_modules('cron')

    if PROJECT_MODULE:
        if '.' in PROJECT_MODULE.__name__:
            try:
                import_module('%s.cron' % '.'.join(
                    PROJECT_MODULE.__name__.split('.')[0:-1]))
            except ImportError as e:
                if 'No module named' not in str(e):
                    print(e)

    # load django tasks
    for cmd, app in get_commands().items():
        try:
            load_command_class(app, cmd)
        except django.core.exceptions.ImproperlyConfigured:
            pass

KRONOS_TEMPLATE = \
    '%(prefix)s %(python)s %(manage)s ' \
    'runtask %(name)s %(passed_args)s --settings=%(settings_module)s ' \
    '%(postfix)s'

DJANGO_TEMPLATE = \
    '%(prefix)s %(python)s %(manage)s ' \
    '%(name)s %(passed_args)s --settings=%(settings_module)s ' \
    '%(postfix)s'

def process_args(args):
    res = []
    for key, value in six.iteritems(args):
        if isinstance(value, dict):
            raise TypeError('Parse for dict arguments not yet implemented.')

        if isinstance(value, list):
            temp_args = ",".join(map(str, value))
            res.append("{}={}".format(key, temp_args))
        else:
            if value is None:
                arg_text = "{}"
            elif isinstance(value, str):
                arg_text = '{}="{}"'
            else:
                arg_text = '{}={}'
            res.append(arg_text.format(key, value))
    return res

def register(schedule, args=None):
    def decorator(function):
        global registry_kronos, registry_django

        passed_args = process_args(args) if args is not None else []

        ctx = {
            'prefix': KRONOS_PREFIX,
            'python': KRONOS_PYTHON,
            'manage': KRONOS_MANAGE,
            'passed_args': ' '.join(passed_args),
            'settings_module': settings.SETTINGS_MODULE,
            'postfix': KRONOS_POSTFIX
        }

        if hasattr(function, 'handle'):
            func = None
            tmpl = DJANGO_TEMPLATE
            name = function.__module__.split('.')[-1]
        else:
            func = function
            tmpl = KRONOS_TEMPLATE
            name = function.__name__

        command = tmpl % dict(ctx, name=name)
        if KRONOS_PYTHONPATH is not None:
            command += ' --pythonpath=%s' % KRONOS_PYTHONPATH

        registry.add(Task(name, schedule, command, func))

        @wraps(function)
        def wrapper(*args, **kwargs):
            return function(*args, **kwargs)
        return wrapper

    return decorator

def install():
    """
    Register tasks with cron.
    """
    load()
    tab = crontab.CronTab(user=True)
    for task in registry:
        tab.new(task.command, KRONOS_BREADCRUMB).setall(task.schedule)
    tab.write()
    return len(registry)

def printtasks():
    """
    Print the tasks that would be installed in the
    crontab, for debugging purposes.
    """
    load()

    tab = crontab.CronTab('')
    for task in registry:
        tab.new(task.command, KRONOS_BREADCRUMB).setall(task.schedule)
    print(tab.render())

def uninstall():
    """
    Uninstall tasks from cron.
    """
    tab = crontab.CronTab(user=True)
    count = len(list(tab.find_comment(KRONOS_BREADCRUMB)))
    tab.remove_all(comment=KRONOS_BREADCRUMB)
    tab.write()
    return count

def reinstall():
    return uninstall(), install()


import logging
import os
import re
import yaml
from abc import ABCMeta, abstractmethod

PROJECT_CONFIG_EXTENSION = "yaml"

logger = logging.getLogger(__name__)

class ProjectHandlerFactory(object):

    handlers = {}

    def __init__(self):
        self.schema_pattern = re.compile("^({}):\/\/(.+)".format('|'.join(list(self.handlers.keys()))))

    def create(self, uri, local_path):
        """Create a project handler

        Args:
            uri (str): schema://something formatted uri
            local_path (str): the project configs directory

        Return:
            ProjectHandler derived class instance
        """
        matches = self.schema_pattern.search(uri)

        if not matches:
            logger.error("Unknown uri schema: '%s'. Added schemas: %s", uri, list(self.handlers.keys()))
            return None

        schema = matches.group(1)
        url = matches.group(2)

        return self.handlers[schema](url, local_path)

def register_schema(schema):
    def decorator(cls):
        ProjectHandlerFactory.handlers[schema] = cls
        return cls
    return decorator

class ProjectHandlerBase(object, metaclass=ABCMeta):

    def __init__(self, url, path):
        self.url = url
        self.path = self.get_path(path)

    def get_project_config_path(self, name):
        base_path = os.path.expanduser(self.path)
        return os.path.join(base_path, "{}.{}".format(name, PROJECT_CONFIG_EXTENSION))

    def load(self):
        """Load the projects config data from local path

        Returns:
            Dict: project_name -> project_data
        """
        projects = {}

        path = os.path.expanduser(self.path)

        if not os.path.isdir(path):
            return projects

        logger.debug("Load project configs from %s", path)

        for filename in os.listdir(path):
            filename_parts = os.path.splitext(filename)

            if filename_parts[1][1:] != PROJECT_CONFIG_EXTENSION:
                continue
            name = filename_parts[0]

            try:
                project_file_path = os.path.join(path, filename)
                with open(project_file_path) as f:
                    data = yaml.load(f)
                projects[name] = data
            except ValueError:
                continue

            logger.debug("Project '{}' config readed from {}".format(name, project_file_path))

        return projects

    def save(self, projects):
        """Save the projects configs to local path

        Args:
            projects (dict): project_name -> project_data
        """

        base_path = os.path.expanduser(self.path)

        if not os.path.isdir(base_path):
            return

        logger.debug("Save projects config to %s", base_path)

        for name, data in list(projects.items()):
            project_file_path = self.get_project_config_path(name)
            with open(project_file_path, "w") as f:
                yaml.dump(data, stream = f, default_flow_style = False)
                logger.debug("Project '%s' config has been writed to '%s'", name, project_file_path)

    @abstractmethod
    def update(self):
        pass

    @abstractmethod
    def get_path(self, path):
        pass

    @abstractmethod
    def config_init(self):
        pass

    @abstractmethod
    def post_process_cli_config(self, config):
        pass

    @abstractmethod
    def edit(self, name, summary, command_result):
        pass

    @abstractmethod
    def create(self, name, default, command_result):
        pass

    @abstractmethod
    def remove(self, name, command_result):
        pass


from datetime import datetime
import logging
from functools import wraps
from subprocess import Popen, CalledProcessError, PIPE
import yaml

from .colors import Colors

class ColoredFormatter(logging.Formatter):

    # TODO: debug_mode to ctor?

    def __init__(self, debug):
        super(ColoredFormatter, self).__init__()
        self.debug = debug

    def format(self, record):
        colors = {
            logging.ERROR: Colors.red,
            logging.WARNING: Colors.yellow,
            logging.INFO: Colors.default,
            logging.DEBUG: Colors.cyan
        }

        if self.debug:
            msg = '%s - %s:%s: ' % (datetime.now(), record.name, record.lineno)
            msg += Colors.default + record.getMessage()
        else:
            msg = record.getMessage()

        msg = colors.get(record.levelno, Colors.default) + msg + Colors.default

        if record.exc_info:
            msg += '\n' + self.formatException(record.exc_info)

        return msg

def confirm_prompt(msg = "Are you sure?", help = "(ok: enter, cancel: ctrl+c)"):
    try:
        input("{} {} ".format(msg, help))
        return True
    except KeyboardInterrupt:
        # for new line
        print('')
        return False

def get_func_defaults(func):
    diff = len(func.__code__.co_varnames) - len(func.__defaults__)
    return {func.__code__.co_varnames[diff+idx]: val for idx, val in enumerate(func.__defaults__)}

def confirm(msg = get_func_defaults(confirm_prompt)['msg'], help = get_func_defaults(confirm_prompt)['help']):

    def decor(func):

        @wraps(func)
        def wrapper(*args, **kwargs):
            if not kwargs.get('iamsure', False) and not confirm_prompt(msg, help):
                return
            func(*args, **kwargs)

        return wrapper

    return decor

def yaml_add_object_hook_pairs(type_):
    _mapping_tag = yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG

    def dict_representer(dumper, data):
        return dumper.represent_dict(iter(data.items()))

    def dict_constructor(loader, node):
        return type_(loader.construct_pairs(node))

    yaml.add_representer(type_, dict_representer)
    yaml.add_constructor(_mapping_tag, dict_constructor)

def check_call(command, **kwargs):
    p = Popen(command, stderr = PIPE, stdout = PIPE, **kwargs)
    stdout, stderr = p.communicate()
    if p.returncode != 0:
        raise CalledProcessError(p.returncode, command, stderr)

def define_singleton(carrier, name, cls, cls_args = {}):
    """Creates a property with the given name, but the cls will created only with the first call

    Args:
        carrier: an instance of the class where want to reach the cls instance
        name (str): the variable name of the cls instance
        cls (type): the singleton object type
        cls_args (dict): optional dict for createing cls

    """
    instance_name = "__{}".format(name)
    setattr(carrier, instance_name, None)

    def getter(self):
        instance = getattr(carrier, instance_name)

        if instance is None:
            instance = cls(**cls_args)
            setattr(carrier, instance_name, instance)

        return instance

    setattr(type(carrier), name, property(getter))


import os
import shutil
import logging
from voidpp_tools.terminal import get_size
from collections import OrderedDict
import re
from datetime import timedelta, datetime

from .repository_command_result_box import RepositoryCommandResultBox
from .exceptions import ProjectException, RepositoryCommandException
from .project_languages import LanguageFactory

logger = logging.getLogger(__name__)

class TopologicalSorter(object):
    """
    Implements Tarjan's algorithm.
    """
    def __init__(self, nodes):
        self.nodes = nodes
        self.marks = {n: None for n in self.nodes}
        self.result = []

    def get_mark(self, node):
        return self.marks[node.name]

    def has_temp_mark(self, node):
        return self.get_mark(node) == 'temp'

    def has_permanent_mark(self, node):
        return self.get_mark(node) == 'permanent'

    def sort(self):
        while True:
            has_unmarked = False
            for name, node in list(self.nodes.items()):
                if not self.has_permanent_mark(node):
                    has_unmarked = True
                    self.__visit(node)

            if not has_unmarked:
                return self.result

    def __visit(self, node):
        if self.has_temp_mark(node):
            return

        if not self.has_permanent_mark(node):
            self.marks[node.name] = 'temp'
            for dep in list(node.get_dependent_projects().values()):
                self.__visit(dep)
            self.marks[node.name] = 'permanent'
            self.result.insert(0, node)

class Project(object):

    def __init__(self, name, vcp, data = None):
        self.name = name
        self.description = None
        self.languages = []
        self.repo = dict(
            url = None,
            type = None,
        )
        self.dependencies = {}
        self.system_dependencies = {}
        self.vcp = vcp

        if data:
            self.data = data

    @property
    def last_status(self):
        for lang in self.languages:
            if not lang.env.last_status:
                return False
        return True

    @property
    def repositories(self):
        projects = list(self.get_dependent_projects().values()) + [self]
        return [p.name for p in projects]

    @property
    def path(self):
        try:
            return self.vcp.repositories[self.name].path
        except KeyError:
            return None

    @property
    def initialized(self):
        return self.name in self.vcp.repositories

    @property
    def data(self):
        return OrderedDict([
            ('description', self.description),
            ('dependencies', self.dependencies),
            ('repo', self.repo),
            ('languages', [l.name for l in self.languages]),
            ('system_dependencies', self.system_dependencies),
        ])

    @data.setter
    def data(self, value):
        self.description = value['description']
        self.repo = value['repo']
        self.dependencies = value['dependencies']
        self.system_dependencies = value['system_dependencies']
        self.languages = self.vcp.language_factory.create(self, value['languages'])

    def set_dependencies_state(self):
        for name in self.get_dependent_projects(recursive = False):
            ref = self.dependencies[name]
            try:
                self.vcp.repositories[name].set_ref(ref)
                logger.info("Set ref '%s' for '%s'", ref, name)
            except RepositoryCommandException as e:
                logger.error(e.output)

    def search_for_ref_in_deps(self, project_name, projects):
        ref = None
        refprj = None
        for prj in projects:
            depref = prj.dependencies.get(project_name)
            if depref is None:
                continue
            if ref is not None and ref != depref:
                raise Exception("Found multiple refs for dep '{}', {}:{} and {}:{} ".format(project_name, prj.name, depref, refprj, ref))
            ref = depref
            refprj = prj.name

        if ref is None:
            raise Exception("Cannot find ref for '{}' but it's impossible!".format(project_name))

        return ref

    def init(self, base_path, status, force = False, install_deps = True, init_languages = True, ref = 'master'):

        repo_exists = self.name in self.vcp.repositories

        logger.debug("Start project init: '%s', force = %s, install_deps = %s, init_languages = %s", self.name, force, install_deps, init_languages)

        if repo_exists and not force:
            logger.debug("Project '%s' has been initialized already, skipping...", self.name)
            return True

        repo_dir = os.path.join(base_path, self.name)

        if install_deps:
            projects = self.get_sorted_dependencies()

            logger.info("Dependencies of %s: %s", self.name, [p.name for p in projects])

            for project in reversed(projects):
                try:
                    ref = self.dependencies[project.name]
                except KeyError as e:
                    ref = self.search_for_ref_in_deps(project.name, projects)
                if not project.init(base_path, status, force, install_deps = False, init_languages = init_languages, ref = ref):
                    return False

        if not repo_exists or init_languages:
            tw = get_size()['cols']
            label = "< Initializie {} >".format(self.name)
            pre_len = 10
            logger.info('\n' + '-' * pre_len + label + '-' * (tw - pre_len - len(label)))
            logger.info("URL: '{}', path: '{}'".format(self.repo['url'], repo_dir))

        status[self.name] = True

        try:
            if self.vcp.system_package_manager_handler:
                packages = self.vcp.system_package_manager_handler.get_not_installed_packages(self)
                if len(packages):
                    logger.error("Need to install these system packages: %s", ', '.join(packages))
                    return False

            # create repo config
            repo = self.vcp.repo_factory.create(repo_dir, self.repo['type'], self.name)

            if repo_exists:
                repo.update()
            else:
                # create folder
                os.mkdir(repo_dir)

                repo.init(self.repo['url'], ref)
                self.vcp.repositories[self.name] = repo

            # initialize language specific stuffs
            if not repo_exists or init_languages:
                for lang in self.languages:
                    lang.init()
                    if not lang.env.get_status():
                        status[self.name] = False

            return True
        except (Exception, KeyboardInterrupt) as e:
            logger.exception("Error during initialize '{}'. Reverting all the work. Traceback:".format(self.name))
            self.purge()
            return False

    def purge(self):
        path = self.path

        logger.info("Purge project '{}'".format(self.name))

        for lang in self.languages:
            lang.purge()

        if self.name in self.vcp.repositories:
            del self.vcp.repositories[self.name]
            logger.debug("Delete repository config")

        if path and os.path.isdir(path):
            shutil.rmtree(path)
            logger.debug("Delete repository directory '%s'", path)

    def install_to(self, project, env):
        for lang in self.languages:
            if type(env) == type(lang.env):
                lang.install_to(project, env)

    def get_sorted_dependencies(self, remove_indirect_deps = False):
        topo = TopologicalSorter(self.get_dependent_projects())

        all_dep = topo.sort()

        if remove_indirect_deps:
            for prj in all_dep:
                if prj.name not in self.dependencies:
                    all_dep.remove(prj)

        return all_dep

    def get_dependent_projects(self, recursive = True):
        """Get the dependencies of the Project

        Args:
            recursive (bool): add the dependant project's dependencies too

        Returns:
            dict of project name and project instances
        """
        projects = {}
        for name, ref in list(self.dependencies.items()):
            try:
                prj = self.vcp.projects[name]
            except KeyError:
                logger.error("Unknown project '%s' in project '%s' dependencies!", name, self.name)
                continue
            projects[name] = prj
            if recursive:
                projects.update(prj.get_dependent_projects())
        return projects

    def news(self, fromcache):
        for name in self.repositories:
            repo = self.vcp.repositories[name]
            if not fromcache:
                repo.fetch()
            commits = repo.get_new_commits()
            if len(commits):
                yield RepositoryCommandResultBox(repo, "\n".join(commits))

    def unreleased(self):
        for name in self.repositories:
            repo = self.vcp.repositories[name]
            commits = repo.get_commits_from_last_tag()
            if len(commits):
                yield RepositoryCommandResultBox(repo, "\n".join(commits))

    def diff(self):
        for name in self.repositories:
            repo = self.vcp.repositories[name]
            diff = repo.diff()
            if len(diff):
                yield RepositoryCommandResultBox(repo, diff)

    def pushables(self, remote):
        for name in self.repositories:
            repo = self.vcp.repositories[name]
            commits = repo.pushables(remote)
            if len(commits):
                yield RepositoryCommandResultBox(repo, "\n".join(commits))

    def untracked(self):
        for name in self.repositories:
            repo = self.vcp.repositories[name]
            files = repo.get_untracked_files()
            if len(files):
                yield RepositoryCommandResultBox(repo, "\n".join(files))

    def dirty(self):
        for name in self.repositories:
            repo = self.vcp.repositories[name]
            files = repo.get_dirty_files()
            if len(files):
                yield RepositoryCommandResultBox(repo, "\n".join(files))

    def fetch(self):
        for name in self.repositories:
            repo = self.vcp.repositories[name]
            res = repo.fetch()
            if len(res):
                yield RepositoryCommandResultBox(repo, res)

    def standup(self, length):
        lengths = OrderedDict([
            ('w', 60 * 24 * 7),
            ('d', 60 * 24),
            ('h', 60),
            ('m', 1),
        ])

        decode_pattern = re.compile(''.join(['([\d]+{})?'.format(k) for k in lengths]))
        res = decode_pattern.match(length)
        if not res:
            raise Exception('go back')

        length_items = list(lengths)

        value = 0
        for idx, grp in enumerate(res.groups()):
            if grp is None:
                continue
            abbr = length_items[idx]
            val = int(grp[:-1])
            value += val * lengths[abbr]

        time_len = timedelta(minutes = value)

        since = datetime.now() - time_len

        for name in self.repositories:
            repo = self.vcp.repositories[name]
            res = repo.get_own_commits_since(since.isoformat())
            if len(res):
                yield RepositoryCommandResultBox(repo, res)

    def status(self):
        for name in self.repositories:
            repo = self.vcp.repositories[name]
            yield RepositoryCommandResultBox(repo, repo.status())

    def reset(self):
        for name in self.repositories:
            repo = self.vcp.repositories[name]
            yield RepositoryCommandResultBox(repo, repo.reset())

    def cmd(self, command):
        for repo_name in self.repositories:
            repo = self.vcp.repositories[repo_name]
            res = repo.cmd(command)
            if len(res):
                yield RepositoryCommandResultBox(repo, res)

    def __repr__(self):
        return "<Project: %s>" % self.__dict__


import logging
import tempfile
import yaml
import os
import shutil
from functools import wraps
from subprocess import check_call, CalledProcessError
from prettytable import PrettyTable

from .project import Project
from .exceptions import RepositoryException, ProjectException
from .tools import confirm, confirm_prompt

logger = logging.getLogger(__name__)

def post_process(func):
    """Calls the project handler same named function

    Note: the project handler may add some extra arguments to the command,
          so when use this decorator, add **kwargs to the end of the arguments

    """
    @wraps(func)
    def wrapper(*args, **kwargs):
        res = func(*args, **kwargs)
        project_command = args[0]
        project_handler = project_command.vcp.project_handler
        if not project_handler:
            return res
        kwargs['command_result'] = res
        getattr(project_handler, func.__name__)(**kwargs)
        return res

    return wrapper

class ProjectConfigCommand(object):
    def __init__(self, vcp):
        self.vcp = vcp

    def uri(self, value):
        matches = self.vcp.project_handler_factory.schema_pattern.search(value)

        if not matches:
            logger.error("Unknown uri schema: '{}'".format(value))
            return

        self.vcp.projects_reference['uri'] = value

        try:
            project_handler = self.vcp.project_handler_factory.create(value, self.vcp.projects_reference['path'])
            project_handler.config_init()
        except Exception as e:
            logger.error("Unknown error: {}".format(e), exc_info = e)
            return

        self.vcp.save_config()
        logger.info("Project config storage uri has been set!")

    def path(self, value):
        self.vcp.projects_reference['path'] = value
        self.vcp.save_config()

    def update(self):
        self.vcp.project_handler.update()

class ProjectCommand(object):
    def __init__(self, vcp):
        self.vcp = vcp

    def __init(self, project, path, force, init_languages):
        status = {}
        """
        REFACTOR status to project init result ENUM
        jelenleg ha a project init False, akkor torlunk minden adatot a projectrol
        de van egy atmenet, mikor csak a lang init nem sikerult
        erre valo jelenleg a status. ez rossz
        """

        project.init(path, status, force, init_languages = init_languages)

        failed = []
        for name, val in list(status.items()):
            if val is False and name not in failed:
                failed.append(name)

        return failed

    def __search_sub_path_in_repos(self, path):
        search = os.path.realpath(path) + os.sep
        for name, repo in list(self.vcp.repositories.items()):
            repo_path = os.path.realpath(repo.path) + os.sep
            if search.startswith(repo_path):
                print(repo_path, search)
                return repo
        return None

    def __confirm_init_path(self, path):
        repo = self.__search_sub_path_in_repos(path)
        if repo:
            if not confirm_prompt("The given path is under the '{}' repo. Do you really want to initialize the new project(s) to it?".format(repo.name)):
                return False
        return True

    def __default(self, name):
        self.vcp.default_project = name
        logger.info("Project '%s' set for default project" % name)

    def __edit(self, project):
        with tempfile.NamedTemporaryFile(suffix = '.yaml', mode = "rw+") as fd:

            # write project data to the tmp file
            yaml.dump(project.data, stream = fd, default_flow_style = False)
            fd.flush()

            # open editor with tmp file
            editor = os.getenv('EDITOR', default = 'vim')
            try:
                check_call([editor, fd.name])
            except CalledProcessError as e:
                logger.error("Error occured when calling editor: {}".format(e.output))
                return False

            # read back the tmp file content
            fd.seek(0, 0)
            data = yaml.load(fd)

            # save data if changed
            if data == project.data:
                return False

            unknown_deps = []
            for name, ref in list(data['dependencies'].items()):
                if name not in self.vcp.projects:
                    unknown_deps.append(name)

            if len(unknown_deps):
                logger.error("Unknown dependencies: {}".format(unknown_deps))
                return False

            project.data = data

        self.vcp.save_config()

        return True

    def init(self, name, path, force = False, init_languages = True):
        if not self.__confirm_init_path(path):
            return

        project = self.vcp.projects[name]
        tries = 5

        for i in range(tries):

            failed = self.__init(project, path, force, init_languages)

            if not len(failed):
                break

            logger.error("In {} projects the init was not successfull ({}). Try again! ({}/{})".format(len(failed), ', '.join(failed), i+1,tries))

            if i+1 == tries:
                logger.error("No more attempt...")

            force = True

        self.vcp.save_config()

    def update(self, name, path):
        self.vcp.project_handler.update()
        self.init(name, path, force = True, init_languages = True)

    def config(self):
        return ProjectConfigCommand(self.vcp)

    def workon(self, name):
        self.__default(name)
        prj = self.vcp.projects[name]
        prj.set_dependencies_state()
        self.vcp.save_config()

    def show(self, name):
        prj = self.vcp.projects[name]
        logger.info(yaml.dump(prj.data, default_flow_style = False))

    @post_process
    def remove(self, name, **kwargs):
        # TODO: check other projects dependencies
        del self.vcp.projects[name]
        os.remove(self.vcp.project_handler.get_project_config_path(name))
        logger.info("Project removed")
        self.vcp.save_project_config()

    @post_process
    def edit(self, name, summary, **kwargs):
        res = self.__edit(self.vcp.projects[name])
        if res:
            logger.info("Project modified")
            self.vcp.save_project_config()
        return res

    @post_process
    def create(self, name, default = False, **kwargs):
        if name in self.vcp.projects:
            raise ProjectException("Project '%s' is already exists" % name)

        if default:
            self.vcp.default_project = name

        prj = Project(name, self.vcp)

        self.vcp.projects[name] = prj

        res = self.__edit(prj)
        if res:
            logger.info("Project created")
            self.vcp.save_project_config()
        return res

    def list(self):
        table = PrettyTable(["Name", "Description", "Deps", "Languages", "Initialized"])
        table.align = 'l'
        table.align["Deps"] = "r"
        projects = list(self.vcp.projects.values())
        for project in sorted(projects, key = lambda p: p.name):
            table.add_row([
                project.name,
                project.description,
                len(project.get_dependent_projects()),
                ', '.join(project.data['languages']),
                project.initialized,
            ])

        logger.info("Known projects ({}):\n{}".format(len(projects), table))

    def default(self, name):
        self.__default(name)
        self.vcp.save_config()

    @confirm()
    def purge(self, name, iamsure):
        project = self.vcp.projects[name]
        project.purge()

        self.vcp.save_config()

    @confirm()
    @confirm('Really?')
    def purge_all(self, iamsure):
        for name, project in list(self.vcp.projects.items()):
            project.purge()

        self.vcp.save_config()

class NPMConfigCommand(object):
    def __init__(self, vcp):
        self.vcp = vcp

    def set(self, name, value):
        self.vcp.npm_config[name] = value
        self.vcp.save_config()
        logger.info("Npm config saved")

    def unset(self, name):
        del self.vcp.npm_config[name]
        self.vcp.save_config()
        logger.info("Npm config saved")

    def list(self):
        if not len(self.vcp.npm_config):
            logger.info("Empty")

        table = PrettyTable(["Name", "Value"])
        for name in sorted(self.vcp.npm_config.keys()):
            table.add_row([name, self.vcp.npm_config[name]])
        logger.info("NPM config:\n{}".format(table))

class RepositoryCommand(object):
    def __init__(self, vcp):
        self.vcp = vcp

    def cmd(self, name, command):
        logger.info(self.vcp.repositories[name].cmd(command))

    def show_path(self, name):
        print((self.vcp.repositories[name].path))

    def list(self, format):
        # TODO: refactor this output format to sg generic, which appliable to the whole vcp
        if format == 'table':
            table = PrettyTable(["Type", "Name", "Path"])
            table.align = 'l'
            for repo_name in sorted(self.vcp.repositories):
                repo = self.vcp.repositories[repo_name]
                table.add_row([repo.type, repo.name, repo.path])
            logger.info("Known repositories:\n{}".format(table))
        else:
            # logger prints color codes, but this format use for bash tab completion
            print(('\n'.join(sorted(self.vcp.repositories.keys()))))

    def remove(self, name):
        del self.vcp.repositories[name]
        logger.info("Repository '{}' removed".format(name))
        self.vcp.save_config()

    @confirm()
    def clear(self, iamsure):
        self.vcp.repositories = {}
        self.vcp.save_config()
        logger.info("Repositories has been removed.")

    def create(self, path, type, name, add_to = None):
        path = os.path.abspath(path)
        if not os.path.isdir(path):
            raise RepositoryException("Path '%s' is not exists" % path)
        if name is None:
            name = path.split(os.path.sep)[-1:][0]

        if name in self.vcp.repositories:
            raise ProjectException("Repository '%s' is already exists" % name)

        self.vcp.repositories[name] = self.vcp.repo_factory.create(path, type, name)

        msg = "Repository '%s' created" % name

        if add_to is not None:
            add_to = set(add_to)
            for prj_name in add_to:
                self.vcp.projects[prj_name].repositories.append(name)
            msg += " and added to project: " + ', '.join(add_to)

        logger.info(msg)
        self.vcp.save_config()

class PackageCommand(object):

    def __init__(self, vcp):
        self.vcp = vcp

    def init(self, language):
        package_handler = self.vcp.package_factory.create(language)
        package_handler.init()

    def install_requirements(self, language):
        if language is None:
            for handler in self.vcp.package_factory.create_all():
                handler.install_requirements()
        else:
            package_handler = self.vcp.package_factory.create(language)
            package_handler.install_requirements()


'''This module contains functions that you can hook into to allow various
lenses to operate on your own custom data structures.

You can hook into them by defining a method that starts with
``_lens_`` followed by the name of the hook function. So, for
example: the hook for ``lenses.hooks.contains_add`` is a method called
``_lens_contains_add``. This is the preferred way of hooking into this
library because it does not require you to have the lenses library as
a hard dependency.

These functions are all decorated with ``singledispatch``, allowing
you to customise the behaviour of types that you did not write. Be
warned that single dispatch functions are registered globally across
your program and that your function also needs to be able to deal with
subclasses of any types you register (or else register separate functions
for each subclass).

All of these hooks operate in the following order:

* Use an implementation registered with ``singledispatch.register``
  for the relevant type, if one exists.
* Use the relevant ``_lens_*`` method on the first object that was passed
  in, if it exists.
* Use a default implementation that is likely to work for most python
  objects, if one exists.
* Raise ``NotImplementedError``.
'''

from typing import Any, Dict, FrozenSet, Iterable, Iterator, List, Set, Tuple
from singledispatch import singledispatch
import copy
import sys

from ..typevars import A, B

if sys.version_info[0] > 2:
    from builtins import setattr as builtin_setattr
else:
    builtin_setattr = setattr

@singledispatch
def setitem(self, key, value):
    # type: (Any, Any, Any) -> Any
    '''Takes an object, a key, and a value and produces a new object
    that is a copy of the original but with ``value`` as the new value of
    ``key``.

    The following equality should hold for your definition:

    .. code-block:: python

        setitem(obj, key, obj[key]) == obj

    This function is used by many lenses (particularly GetitemLens) to
    set items on states even when those states do not ordinarily support
    ``setitem``. This function is designed to have a similar signature
    as python's built-in ``setitem`` except that it returns a new object
    that has the item set rather than mutating the object in place.

    It's what enables the ``lens[some_key]`` functionality.

    The corresponding method call for this hook is
    ``obj._lens_setitem(key, value)``.

    The default implementation makes a copy of the object using
    ``copy.copy`` and then mutates the new object by setting the item
    on it in the conventional way.
    '''
    try:
        self._lens_setitem
    except AttributeError:
        selfcopy = copy.copy(self)
        selfcopy[key] = value
        return selfcopy
    else:
        return self._lens_setitem(key, value)

if sys.version_info[0] > 2:

    @setitem.register(bytes)
    def _bytes_setitem(self, key, value):
        # type: (bytes, int, int) -> bytes
        data = bytearray(self)
        data[key] = value
        return bytes(data)

    @setitem.register(str)
    def _str_setitem(self, key, value):
        # type: (str, int, str) -> str
        data = list(self)
        data[key] = value
        return ''.join(data)

else:

    @setitem.register(str)
    def _bytes_setitem(self, key, value):
        # type: (str, int, int) -> str
        data = bytearray(self)
        data[key] = value
        return bytes(data)

    @setitem.register(unicode)
    def _str_setitem(self, key, value):
        # type: (unicode, int, int) -> unicode
        data = list(self)
        data[key] = value
        return ''.join(data)

@setitem.register(tuple)
def _tuple_setitem(self, key, value):
    # type: (Tuple[A, ...], int, A) -> Tuple[A, ...]
    return tuple(value if i == key else item for i, item in enumerate(self))

@singledispatch
def setattr(self, name, value):
    # type: (Any, Any, Any) -> Any
    '''Takes an object, a string, and a value and produces a new object
    that is a copy of the original but with the attribute called ``name``
    set to ``value``.

    The following equality should hold for your definition:

    .. code-block:: python

        setattr(obj, 'attr', obj.attr) == obj

    This function is used by many lenses (particularly GetattrLens) to set
    attributes on states even when those states do not ordinarily support
    ``setattr``. This function is designed to have a similar signature
    as python's built-in ``setattr`` except that it returns a new object
    that has the attribute set rather than mutating the object in place.

    It's what enables the ``lens.some_attribute`` functionality.

    The corresponding method call for this hook is
    ``obj._lens_setattr(name, value)``.

    The default implementation makes a copy of the object using
    ``copy.copy`` and then mutates the new object by calling python's
    built in ``setattr`` on it.
    '''
    try:
        self._lens_setattr
    except AttributeError:
        selfcopy = copy.copy(self)
        builtin_setattr(selfcopy, name, value)
        return selfcopy
    else:
        return self._lens_setattr(name, value)

@setattr.register(tuple)
def _tuple_setattr_immutable(self, name, value):
    # type: (Any, str, A) -> Any
    # setting attributes on a tuple probably means we really have a
    # namedtuple so we can use self._fields to understand the names
    data = (
        value if field == name else item
        for field, item in zip(self._fields, self)
    )
    return type(self)(*data)

@singledispatch
def contains_add(self, item):
    # type (Any, Any) -> Any
    '''Takes a collection and an item and returns a new collection
    of the same type that contains the item. The notion of "contains"
    is defined by the object itself; The following must be ``True``:

    .. code-block:: python

        item in contains_add(obj, item)

    This function is used by some lenses (particularly ContainsLens)
    to add new items to containers when necessary.

    The corresponding method call for this hook is
    ``obj._lens_contains_add(item)``.

    There is no default implementation.
    '''
    try:
        self._lens_contains_add
    except AttributeError:
        message = 'Don\'t know how to add an item to {}'
        raise NotImplementedError(message.format(type(self)))
    else:
        return self._lens_contains_add(item)

@contains_add.register(list)
def _list_contains_add(self, item):
    # type (List[A], A) -> List[A]
    return self + [item]

@contains_add.register(tuple)
def _tuple_contains_add(self, item):
    # type (Tuple[A, ...], A) -> Tuple[A, ...]
    return self + (item,)

@contains_add.register(dict)
def _dict_contains_add(self, item):
    # type (Dict[A, B], A) -> Dict[A, B]
    result = self.copy()
    result[item] = None
    return result

@contains_add.register(set)
def _set_contains_add(self, item):
    # type (Set[A], A) -> Set[A]
    return self | {item}

@singledispatch
def contains_remove(self, item):
    # type (Any, Any) -> Any
    '''Takes a collection and an item and returns a new collection
    of the same type with that item removed. The notion of "contains"
    is defined by the object itself; the following must be ``True``:

    .. code-block:: python

        item not in contains_remove(obj, item)

    This function is used by some lenses (particularly ContainsLens)
    to remove items from containers when necessary.

    The corresponding method call for this hook is
    ``obj._lens_contains_remove(item)``.

    There is no default implementation.
    '''
    try:
        self._lens_contains_remove
    except AttributeError:
        message = 'Don\'t know how to remove an item from {}'
        raise NotImplementedError(message.format(type(self)))
    else:
        return self._lens_contains_remove(item)

@contains_remove.register(list)
def _list_contains_remove(self, item):
    # type (List[A], A) -> List[A]
    return [x for x in self if x != item]

@contains_remove.register(tuple)
def _tuple_contains_remove(self, item):
    # type (Tuple[A, ...], A) -> Tuple[A, ...]
    return tuple(x for x in self if x != item)

@contains_remove.register(dict)
def _dict_contains_remove(self, item):
    # type (Dict[A, B], A) -> Dict[A, B]
    result = self.copy()
    del result[item]
    return result

@contains_remove.register(set)
def _set_contains_remove(self, item):
    # type (Set[A], A) -> Set[A]
    return self - {item}

@singledispatch
def to_iter(self):
    # type: (Any) -> Any
    '''Takes an object and produces an iterable. It is intended as the
    inverse of the ``from_iter`` function.

    The reason this hook exists is to customise how dictionaries are
    iterated. In order to properly reconstruct a dictionary from an
    iterable you need access to both the keys and the values. So this
    function iterates over dictionaries by thier items instead.

    The corresponding method call for this hook is
    ``obj._lens_to_iter()``.

    The default implementation is to call python's built in ``iter``
    function.
    '''
    try:
        self._lens_to_iter
    except AttributeError:
        return iter(self)
    else:
        return self._lens_to_iter()

@to_iter.register(dict)
def _dict_to_iter(self):
    # type: (Dict[A, B]) -> Iterator[Tuple[A, B]]
    return iter(self.items())

@singledispatch
def from_iter(self, iterable):
    # type: (Any, Any) -> Any
    '''Takes an object and an iterable and produces a new object that is
    a copy of the original with data from ``iterable`` reincorporated. It
    is intended as the inverse of the ``to_iter`` function. Any state in
    ``self`` that is not modelled by the iterable should remain unchanged.

    The following equality should hold for your definition:

    .. code-block:: python

        from_iter(self, to_iter(self)) == self

    This function is used by EachLens to synthesise states from iterables,
    allowing it to focus every element of an iterable state.

    The corresponding method call for this hook is
    ``obj._lens_from_iter(iterable)``.

    There is no default implementation.
    '''
    try:
        self._lens_from_iter
    except AttributeError:
        message = 'Don\'t know how to create instance of {} from iterable'
        raise NotImplementedError(message.format(type(self)))
    else:
        return self._lens_from_iter(iterable)

if sys.version_info[0] > 2:

    @from_iter.register(bytes)
    def _bytes_from_iter(self, iterable):
        # type: (bytes, Iterable[int]) -> bytes
        return bytes(iterable)

    @from_iter.register(str)
    def _str_from_iter(self, iterable):
        # type: (str, Iterable[str]) -> str
        return ''.join(iterable)

else:

    @from_iter.register(str)
    def _bytes_from_iter(self, iterable):
        # type: (str, Iterable[str]) -> str
        return ''.join(iterable)

    @from_iter.register(unicode)
    def _str_from_iter(self, iterable):
        # type: (unicode, Iterable[unicode]) -> unicode
        return u''.join(iterable)

@from_iter.register(dict)
def _dict_from_iter(self, iterable):
    # type: (Dict, Iterable[Tuple[A, B]]) -> Dict[A, B]
    new = self.copy()
    new.clear()
    new.update(iterable)
    return new

@from_iter.register(list)
def _list_from_iter(self, iterable):
    # type: (List, Iterable[A]) -> List[A]
    return list(iterable)

@from_iter.register(set)
def _set_from_iter(self, iterable):
    # type: (Set, Iterable[A]) -> Set[A]
    return set(iterable)

@from_iter.register(frozenset)
def _frozenset_from_iter(self, iterable):
    # type: (FrozenSet, Iterable[A]) -> FrozenSet[A]
    return frozenset(iterable)

@from_iter.register(tuple)
def _tuple_from_iter(self, iterable):
    if type(self) is tuple:
        return tuple(iterable)
    elif hasattr(self, '_make'):
        # this is probably a namedtuple
        return self._make(iterable)
    else:
        message = 'Don\'t know how to create instance of {} from iterable'
        raise NotImplementedError(message.format(type(self)))