# -*- coding: ascii -*-
#
# Copyright 2007, 2008, 2009, 2010, 2011
# Andr\xe9 Malo or his licensors, as applicable
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
==================
 Simple make base
==================

Simple make base.
"""
__author__ = u"Andr\xe9 Malo"
__docformat__ = "restructuredtext en"

import sys as _sys

from _setup import term as _term


class Failure(SystemExit):
    """ Failure exception """


def fail(reason):
    """ Fail for a reason """
    raise Failure(reason)


def warn(message, name=None):
    """ Warn """
    _term.red("%(NAME)sWarning: %(msg)s",
        NAME=name and "%s:" % name or '', msg=message
    )


def fatal(reason):
    """ Fatal error, immediate stop """
    print >> _sys.stderr, reason
    _sys.exit(1)


class Target(object):
    """ Target base class """
    NAME = None
    DEPS = None
    HIDDEN = False

    ERROR = None

    def __init__(self, runner):
        """ Base __init__ """
        self.runner = runner
        self.init()

    def init(self):
        """ Default init hook """
        pass

    def run(self):
        """ Default run hook """
        pass

    def clean(self, scm=True, dist=False):
        """ Default clean hook """
        pass


class _Runner(object):
    """ Runner """

    def __init__(self, *targetscollection):
        """ Initialization """
        tdict = {}
        if not targetscollection:
            import __main__
            targetscollection = [__main__]

        from _setup.make import default_targets
        if default_targets not in targetscollection:
            targetscollection.append(default_targets)

        for targets in targetscollection:
            for value in vars(targets).values():
                if isinstance(value, type) and issubclass(value, Target) and \
                        value.NAME is not None:
                    if value.NAME in tdict:
                        if issubclass(value, tdict[value.NAME]):
                            pass # override base target
                        elif issubclass(tdict[value.NAME], value):
                            continue # found base later. ignore
                        else:
                            warn('Ambiguous target name', value.NAME)
                            continue
                    tdict[value.NAME] = value
        self._tdict = tdict
        self._itdict = {}

    def print_help(self):
        """ Print make help """
        import textwrap as _textwrap

        targets = self.targetinfo()
        keys = []
        for key, info in targets.items():
            if not info['hide']:
                keys.append(key)
        keys.sort()
        length = max(map(len, keys))
        info = []
        for key in keys:
            info.append("%s%s" % (
                (key + " " * length)[:length + 2],
                _textwrap.fill(
                    targets[key]['desc'].strip(),
                    subsequent_indent=" " * (length + 2)
                ),
            ))
        print "Available targets:\n\n" + "\n".join(info)

    def targetinfo(self):
        """ Extract target information """
        result = {}
        for name, cls in self._tdict.items():
            result[name] = {
                'desc': cls.__doc__ or "no description",
                'hide': cls.HIDDEN,
                'deps': cls.DEPS or (),
            }
        return result

    def _topleveltargets(self):
        """ Find all top level targets """
        rev = {} # key is a dep of [values]
        all_ = self.targetinfo()
        for target, info in all_.items():
            for dep in info['deps']:
                if dep not in all_:
                    fatal("Unknown target '%s' (dep of %s) -> exit" % (
                        dep, target
                    ))
                rev.setdefault(dep, []).append(target)
        return [target for target, info in rev.items() if not info]

    def _run(self, target, seen=None):
        """ Run a target """
        if target.DEPS:
            self(*target.DEPS, **{'seen': seen})

        if not target.HIDDEN:
            _term.yellow(">>> %(name)s", name=target.NAME)

        try:
            result = target.run()
        except KeyboardInterrupt:
            result, target.ERROR = False, "^C -> exit"
        except Failure, e:
            result, target.ERROR = False, "%s: %s" % (target.NAME, e)
        except (SystemExit, MemoryError):
            raise
        except:
            import traceback
            target.ERROR = "%s errored:\n%s" % (target.NAME, ''.join(
                traceback.format_exception(*_sys.exc_info())
            ))
            result = False
        else:
            if result is None:
                result = True
        return result

    def _clean(self, target, scm, dist, seen=None):
        """ Run a target """
        if target.DEPS:
            self.run_clean(
                *target.DEPS, **{'scm': scm, 'dist': dist, 'seen': seen}
            )

        try:
            result = target.clean(scm, dist)
        except KeyboardInterrupt:
            result, target.ERROR = False, "^C -> exit"
        except Failure, e:
            result, target.ERROR = False, "%s: %s" % (target.NAME, e)
        except (SystemExit, MemoryError):
            raise
        except:
            import traceback
            target.ERROR = "%s errored:\n%s" % (target.NAME, ''.join(
                traceback.format_exception(*_sys.exc_info())
            ))
            result = False
        else:
            if result is None:
                result = True
        return result

    def _make_init(self, seen):
        """ Make init mapper """
        def init(target):
            """ Return initialized target """
            if target not in seen:
                try:
                    seen[target] = self._tdict[target](self)
                except KeyError:
                    fatal("Unknown target '%s' -> exit" % target)
            else:
                seen[target] = None
            return seen[target]
        return init

    def run_clean(self, *targets, **kwargs):
        """ Run targets """
        def pop(name, default=None):
            """ Pop """
            if name in kwargs:
                value = kwargs[name]
                del kwargs[name]
                if value is None:
                    return default
                return value
            else:
                return default
        seen = pop('seen', {})
        scm = pop('scm', True)
        dist = pop('dist', False)
        if kwargs:
            raise TypeError('Unknown keyword parameters')

        if not targets:
            top_targets = self._topleveltargets()
            targets = self.targetinfo()
            for item in top_targets:
                del targets[item]
            targets = targets.keys()
            targets.sort()
            top_targets.sort()
            targets = top_targets + targets

        init = self._make_init(seen)
        for name in targets:
            target = init(name)
            if target is not None:
                if not self._clean(target, scm=scm, dist=dist, seen=seen):
                    msg = target.ERROR
                    if msg is None:
                        msg = "Clean target %s returned error -> exit" % name
                    fatal(msg)

    def __call__(self, *targets, **kwargs):
        """ Run targets """
        if 'seen' in kwargs:
            seen = kwargs['seen']
            del kwargs['seen']
        else:
            seen = None
        if seen is None:
            seen = self._itdict
        if kwargs:
            raise TypeError('Unknown keyword parameters')

        init = self._make_init(seen)
        for name in targets:
            target = init(name)
            if target is not None:
                if not self._run(target, seen):
                    msg = target.ERROR
                    if msg is None:
                        msg = "Target %s returned error -> exit" % name
                    fatal(msg)


def main(*args, **kwargs):
    """
    main(argv=None, *args, name=None)

    Main start point. This function parses the command line and executes the
    targets given through `argv`. If there are no targets given, a help output
    is generated.

    :Parameters:
      `argv` : sequence
        Command line arguments. If omitted or ``None``, they are picked from
        ``sys.argv``.

      `args` : ``tuple``
        The list of modules with targets. If omitted, ``__main__``
        is imported and treated as target module. Additionally the mechanism
        always adds the `_setup.make` module (this one) to the list in order
        to grab some default targets.

      `name` : ``str``
        Name of the executing module. If omitted or ``None``, ``'__main__'``
        is assumed. If the final name is not ``'__main__'``, the function
        returns immediately.
    """
    try:
        name = kwargs['name']
    except KeyError:
        name = '__main__'
    else:
        del kwargs['name']
        if name is None:
            name = '__main__'

    try:
        argv = kwargs['argv']
    except KeyError:
        if not args:
            args = (None,)
    else:
        del kwargs['argv']
        args = (argv,) + args

    if kwargs:
        raise TypeError("Unrecognized keyword arguments for main()")

    if name == '__main__':
        argv, args = args[0], args[1:]
        if argv is None:
            argv = _sys.argv[1:]

        runner = _Runner(*args)
        if argv:
            runner(*argv)
        else:
            runner.print_help()
