#! 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 get_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():
    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(banner1='')
    else:
        from IPython.terminal.embed import InteractiveShellEmbed
        shell = InteractiveShellEmbed(banner1='')

    return shell


class InitCommand(Command):
    """Create a new session.

    *Additional options*:

    .. cmdoption:: --force

        Create the session even if one already exists with this name.
    """

    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.

    Edit the session file by launching ``$EDITOR`` with the associated Python
    module file. This file can contain any kind of Python code, but you will
    most likely just add device definitions such as this::

        from concert.devices.axes.crio import LinearAxis

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

    def run(self, session=None):
        concert.session.exit_if_not_exists(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.

    If a *session* is not given, the log command shows entries from all
    sessions.
    """
    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 RemoveCommand(Command):
    """Remove one or more sessions.

    .. note::

        Be careful. The session file is unlinked from the file system and no
        backup is made.

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

    Load the session file and launch an IPython shell. Every definition that
    was made in the module file is available via the ``m`` variable. Moreover,
    the quantities package is already loaded and named ``q``. So, once the
    session has started you could access motors like this::

        $ concert start tomo

        This is session tomo
        Welcome to Concert 0.0.1
        In [1]: m.crio1.set_positon(2.23 * q.mm)
        In [2]: m.crio1.get_position()
        Out[2]: array(2.23) * mm

    *Additional options*:

    .. cmdoption:: --logto={stderr, file}

        Specify a method for logging events. If this flag is not specified,
        ``file`` is used and assumed to be
        ``$XDG_DATA_HOME/concert/concert.log``.

    .. cmdoption:: --logfile=<filename>

        Specify a log file if ``--logto`` is set to ``file``.

    """
    def __init__(self):
        opts = {'session': {'type': str},
                '--logto': {'choices': ['stderr', 'file'],
                            'default': 'file'},
                '--logfile': {'type': str}}
        super(StartCommand, self).__init__('start', opts)
        self.logger = None

    def run(self, session=None, logto='file', logfile=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)

        handler.setLevel(logging.DEBUG)
        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(logging.DEBUG)

        # Add session path, so that sessions can import other sessions
        sys.path.append(concert.session.path())
        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

        from concert.quantities import q

        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:
            shell = get_ipython_shell()
            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(),
                RemoveCommand(),
                FetchCommand(),
                StartCommand()]

    commands.extend(plugins)

    for command in commands:
        summary = get_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()
