#! python

import argparse
import contextlib
import logging
import os
import re
import sys
import shutil
import subprocess
import tempfile
import traceback
import concert
import concert.session
from concert.helpers import Command
from concert.ext.cmd import plugins
from concert.base import (UnitError,
                          LimitError,
                          ParameterError,
                          ReadAccessError,
                          WriteAccessError)


def docstring_summary(doc):
    if doc and doc.find('.'):
        return doc[:doc.find('.')]
    return doc


def cmp_versions(v1, v2):
    """Compare two version numbers and return cmp compatible result"""
    def normalize(v):
        return [int(x) for x in re.sub(r'(\.0+)*$', '', v).split(".")]

    n1 = normalize(v1)
    n2 = normalize(v2)
    return (n1 > n2) - (n1 < n2)


def get_ipython_shell(config=None):
    import IPython

    version = IPython.__version__
    shell = None

    # Jeez, let's see what comes next ...
    if cmp_versions(version, '0.11') < 0:
        from IPython.Shell import IPShellEmbed
        shell = IPShellEmbed()
    elif cmp_versions(version, '1.0') < 0:
        from IPython.frontend.terminal.embed import \
            InteractiveShellEmbed
        shell = InteractiveShellEmbed(config=config, banner1='')
    else:
        from IPython.terminal.embed import InteractiveShellEmbed
        shell = InteractiveShellEmbed(config=config, banner1='')

    return shell


def get_prompt_config(session_name):
    """Customize the prompt for the *session_name*."""
    try:
        from IPython.config.loader import Config
        cfg = Config()
        prompt_config = cfg.PromptManager
        trig = ">"
        prompt =  '{} {} '.format(session_name, trig)
        prompt_config.in_template = '{color.Normal}' + prompt
        prompt_config.in2_template = '   .\\D. {} '.format(trig)
        prompt_config.out_template = '\r'.format(trig)
        return cfg
    except:
        pass


class InitCommand(Command):

    """Create a new session."""

    def __init__(self):
        opts = {'session': {'type': str},
                '--force': {'action': 'store_true',
                            'help': "Overwrite existing sessions"},
                '--imports': {'help': "Pre-import processes",
                              'metavar': 'modules',
                              'default': ''}}
        super(InitCommand, self).__init__('init', opts)

    def run(self, session=None, imports="", force=False):
        if concert.session.exists(session) and not force:
            message = "Session `{0}' already exists."
            message += " Use --force to create it anyway."
            print(message.format(session))
        else:
            concert.session.create(session, imports.split())


class EditCommand(Command):

    """Edit a session."""

    def __init__(self):
        opts = {'session': {'type': str}}
        super(EditCommand, self).__init__('edit', opts)

    def run(self, session=None):
        if not concert.session.exists(session):
            print("Session not found, creating {}.".format(session))
            InitCommand().run(session)

        env = os.environ
        editor = env['EDITOR'] if 'EDITOR' in env else 'vi'
        subprocess.call([editor, concert.session.path(session)])


class LogCommand(Command):

    """Show session logs."""

    def __init__(self):
        opts = {'session': {'type': str,
                            'nargs': '?'}}
        super(LogCommand, self).__init__('log', opts)

    def run(self, session=None):
        logfile = concert.session.logfile_path()

        if not os.path.exists(logfile):
            return

        # This is danger zone here because we run subprocess.call with
        # shell=True.  However, the only input that we input is args.session
        # which we check first and the logfile itself.

        if session:
            concert.session.exit_if_not_exists(session)
            cmd = 'grep "{0}:" {1} | less'.format(session, logfile)
        else:
            cmd = 'less {0}'.format(logfile)

        subprocess.call(cmd, shell=True)


class ShowCommand(Command):

    """Show available sessions or details of a given *session*."""

    def __init__(self):
        opts = {'session': {'type': str,
                            'nargs': '?',
                            'default': None,
                            'help': "Show details"}}
        super(ShowCommand, self).__init__('show', opts)

    def run(self, session=None):
        if session:
            try:
                module = concert.session.load(session)
                print(module.__doc__)
            except IOError:
                print("Cannot find {0}".format(session))
            except ImportError as exception:
                print("Cannot import {0}: {1}".format(session, exception))
        else:
            sessions = concert.session.get_existing()
            print("Available sessions:")

            for session in sessions:
                print("  %s" % session)


class MoveCommand(Command):

    """Move session *source* to *target*."""

    def __init__(self):
        opts = {'source': {'type': str,
                           'help': "Name of the source session"},
                'target': {'type': str,
                           'help': "Name of the target session"}}
        super(MoveCommand, self).__init__('mv', opts)

    def run(self, source, target):
        if not concert.session.exists(source):
            sys.exit("`{}' does not exist".format(source))

        if concert.session.exists(target):
            sys.exit("`{}' already exists".format(target))

        concert.session.move(source, target)
        print("Renamed {} -> {}".format(source, target))


class CopyCommand(Command):

    """Copy session *source* to *target*."""

    def __init__(self):
        opts = {'source': {'type': str,
                           'help': "Name of the source session"},
                'target': {'type': str,
                           'help': "Name of the target session"}}
        super(CopyCommand, self).__init__('cp', opts)

    def run(self, source, target):
        if not concert.session.exists(source):
            sys.exit("`{}' does not exist".format(source))

        if concert.session.exists(target):
            sys.exit("`{}' already exists".format(target))

        concert.session.copy(source, target)
        print("Copied {} -> {}".format(source, target))


class RemoveCommand(Command):

    """Remove one or more sessions."""

    def __init__(self):
        opts = {'sessions': {'type': str,
                             'nargs': '+',
                             'metavar': 'session'}}
        super(RemoveCommand, self).__init__('rm', opts)

    def run(self, sessions=[]):
        for session in sessions:
            print("Removing {0}...".format(session))
            concert.session.remove(session)


class FetchCommand(Command):

    """Import an existing *session*."""

    def __init__(self):
        opts = {'url': {'type': str,
                        'help': "Fetch a Python module and save as a session."
                        " Note: Server certificates of HTTPS requests"
                        " are NOT verified!"},
                '--force': {'action': 'store_true',
                            'help': "Overwrite existing sessions"},
                '--repo': {'action': 'store_true',
                           'help':
                           "Checkout Git repository and import all files"}}
        super(FetchCommand, self).__init__('fetch', opts)

    def run(self, url, force=False, repo=False):
        if repo:
            self._fetch_repo(url, force)
        else:
            self._fetch_file(url, force)

    def _fetch_repo(self, url, force):
        path = tempfile.mkdtemp()
        cmd = 'git clone --quiet {0} {1}'.format(url, path)
        proc = subprocess.Popen(cmd.split(), stdout=subprocess.PIPE,
                                stderr=subprocess.PIPE)
        out, err = proc.communicate()

        if proc.returncode != 0:
            sys.exit("Could not clone {0}.".format(url))

        for filename in (x for x in os.listdir(path) if x.endswith('.py')):
            session_name = os.path.basename(filename[:-3])

            if concert.session.exists(session_name) and not force:
                print("`{0}' already exists (use --force to install"
                      " anyway)".format(session_name))
            else:
                print("Add session {0} ...".format(filename[:-3]))
                shutil.copy(os.path.join(path, filename),
                            concert.session.path())

        shutil.rmtree(path)

    def _fetch_file(self, url, force):
        import urllib2

        if not url.endswith('.py'):
            sys.exit("`{0}' is not a Python module".format(url))

        session_name = os.path.basename(url[:-3])

        if concert.session.exists(session_name) and not force:
            sys.exit("`{0}' already exists".format(session_name))

        local_url = self._get_url(url)

        with contextlib.closing(urllib2.urlopen(local_url)) as data:
            with open(concert.session.path(session_name), 'w') as output:
                output.write(data.read())

    def _get_url(self, path_or_url):
        import urlparse

        result = urlparse.urlsplit(path_or_url)

        if result.scheme:
            return path_or_url

        if not os.path.exists(path_or_url):
            sys.exit("Cannot find module `{0}'.".format(path_or_url))

        result = ('file', '', os.path.abspath(path_or_url), '', '')
        return urlparse.urlunsplit(result)


class StartCommand(Command):

    """Start a session."""

    def __init__(self):
        opts = {'session': {'type': str},
                '--logto': {'choices': ['stderr', 'file'],
                            'default': 'file'},
                '--logfile': {'type': str},
                '--loglevel': {'choices': ['debug', 'info', 'warning', 'error',
                                           'critical'],
                               'default': 'info'},
                '--non-interactive': {'action': 'store_true'}}
        super(StartCommand, self).__init__('start', opts)

    def run(self, session=None, non_interactive=False,
            logto='file', logfile=None, loglevel=None):
        concert.session.exit_if_not_exists(session)

        if logto == 'file':
            filename = logfile if logfile else concert.session.logfile_path()
            handler = logging.FileHandler(filename)
        else:
            handler = logging.StreamHandler(sys.stderr)

        numeric_level = getattr(logging, loglevel.upper())

        handler.setLevel(numeric_level)
        logformat = '[%(asctime)s] %(levelname)s: %(name)s: {}: %(message)s'
        formatter = logging.Formatter(logformat.format(session))
        handler.setFormatter(formatter)

        self.logger = logging.getLogger()
        self.logger.addHandler(handler)
        self.logger.setLevel(numeric_level)

        # Add session path, so that sessions can import other sessions
        sys.path.append(concert.session.path())

        if non_interactive:
            execfile(concert.session.path(session), globals())
        else:
            try:
                module = concert.session.load(session)
            except:
                traceback.print_exc()
                sys.exit(1)

            self.run_shell(module)

    def run_shell(self, module=None):
        def _handler(_shell, _etype, evalue, _traceback_, tb_offset=None):
            print("Sorry, {0}".format(str(evalue)))
            return None

        print("Welcome to Concert {0}".format(concert.__version__))

        if module:
            print(module.__doc__)

        attrs = [attr for attr in dir(module) if not attr.startswith('_')]
        mvars = dict((attr, getattr(module, attr)) for attr in attrs)
        globals().update(mvars)

        try:
            cfg = get_prompt_config(module.__name__)
            try:
                shell = get_ipython_shell(config=cfg)
            except AttributeError:
                cfg.PromptManager.in_template =\
                    cfg.PromptManager.in_template.replace('Normal', 'normal')
                shell = get_ipython_shell(config=cfg)

            exceptions = (UnitError, LimitError, ParameterError,
                          ReadAccessError, WriteAccessError)
            shell.set_custom_exc(exceptions, _handler)
            shell()
        except ImportError as exception:
            msg = "You must install IPython to run the Concert shell: {0}"
            print(msg.format(exception))


def main():
    parser = argparse.ArgumentParser()

    parser.add_argument('--version',
                        action='version',
                        version="Concert v%s " % concert.__version__)

    subparsers = parser.add_subparsers(title="Concert commands",
                                       metavar="")

    commands = [InitCommand(),
                EditCommand(),
                LogCommand(),
                ShowCommand(),
                MoveCommand(),
                CopyCommand(),
                RemoveCommand(),
                FetchCommand(),
                StartCommand()]

    commands.extend(plugins)

    for command in commands:
        summary = docstring_summary(command.__doc__)
        cmd_parser = subparsers.add_parser(command.name, help=summary)
        cmd_parser.set_defaults(func=command.run)

        for arg in command.opts.keys():
            cmd_parser.add_argument(arg, **command.opts[arg])

    if len(sys.argv) == 1:
        parser.print_help()
        sys.exit(0)

    args = parser.parse_args()
    func = args.func
    del args.func
    func(**vars(args))


if __name__ == '__main__':
    main()
