from __future__ import print_function
import os
import sys
import shutil
import argparse
import tempfile
import traceback
import subprocess
from termcolor import colored

from . import cli
from . import const
from .ligopath import ARCHIVE_ROOT

############################################################

def nds_connection():
    import nds2
    port = 31200
    NDSSERVER = os.getenv('NDSSERVER')
    if NDSSERVER:
        hostport = NDSSERVER.split(',')[0].split(':')
        host = hostport[0]
        if len(hostport) > 1:
            port = int(hostport[1])
    else:
        raise ValueError('NDSSERVER not specified')
    return nds2.connection(host, port)

############################################################

parser = argparse.ArgumentParser(
    prog = 'guardutil',
    description = "Advanced LIGO Guardian system utility.",
    formatter_class = argparse.RawDescriptionHelpFormatter,
    epilog = """

Add '-h' after individual commands for command help.

Environment variables:
  IFO                   IFO designator (required for most operations)
  SITE                  SITE designator
  USERAPPS_DIR          LIGO "userapps" root path [/opt/rtcds/userapps/release]
  GUARD_MODULE_PATH     override default userapps system module search path
  GUARD_ARCHIVE_ROOT    system archive root directory (may be URL)
""",
    )

cli.parser_add_version(parser)

subparser = parser.add_subparsers(
    title='Commands',
    metavar='<command>',
    dest='cmd',
    #help=argparse.SUPPRESS,
    )
subparser.required = True

def gen_subparser(cmd, func, sys=True):
    help = func.__doc__.split('\n')[0].lower().strip('.')
    desc = func.__doc__
    p = subparser.add_parser(cmd,
                             formatter_class=argparse.RawDescriptionHelpFormatter,
                             help=help,
                             description=desc)
    if sys:
        cli.parser_add_system(p)
    p.set_defaults(func=func)
    return p

############################################################

def command_help(args):
    """Show command help."""
    if args.cmd:
        try:
            p = subparser.choices[args.cmd]
        except KeyError:
            sys.exit("Unknown command: {}".format(args.cmd))
    else:
        p = parser
    p.print_help()

p = gen_subparser("help", command_help, sys=False)
p.add_argument('cmd', metavar='<command>', nargs='?', help="command")


def print_system(args):
    """Print general system information."""
    system = cli.init_system(args, load=False)
    cli.print_system(system)

p = gen_subparser("print", print_system)


def print_states(args):
    """Print state enumeration."""
    system = cli.init_system(args, load=True)
    cli.print_states(system, flag_requests=False,
                     requests_only=args.requests_only)

p = gen_subparser("states", print_states)
p.add_argument('-r', '--requests-only', action='store_true',
               help="output request states only")


def print_edges(args):
    """Print transition edges."""
    system = cli.init_system(args, load=True)
    for edge in system.graph.edges(data=True):
        source, sink, data = edge
        print('({0}, {1}, {2})'.format(source, sink, data['weight']))

p = gen_subparser("edges", print_edges)


def print_path(args):
    """Print system graph path as a list of states."""
    system = cli.init_system(args, load=True)
    for state in system.shortest_path(args.state0, args.state1):
        print(state)

p = gen_subparser("path", print_path)
p.add_argument('state0', help="initial state")
p.add_argument('state1', help="final state")


def draw_graph(args):
    """Draw/display system graph."""
    try:
        import guardian.graph as graph
    except ImportError as e:
        sys.exit("Graph drawing not supported: %s" % e)
    system = cli.init_system(args, load=True)
    if args.all:
        args.gotos = True
        args.jumps = True
    path = ()
    if args.state:
        if args.request:
            path = (args.state, args.request)
        else:
            path = (args.state,)
    for state in path:
        if state not in system:
            sys.exit("State '%s' not defined in system %s." % (state, system.name))

    dot = graph.sys2dot(system,
                        path=path,
                        show_index=args.index,
                        show_gotos=args.gotos,
                        show_jumps=args.jumps,
                        edge_constraints=args.constraints,
                        )

    # display graph or write to file
    default_format = 'pdf'
    if args.outfile is not False:
        outfile = '%s_%s' % (system.ifo, system.name)
        outpath = args.outfile
        if outpath is None:
            outpath = outfile
        elif os.path.isdir(os.path.expanduser(outpath)):
            outpath = os.path.join(os.path.expanduser(outpath), outfile)
        ext = os.path.splitext(outpath)[1]
        if args.format:
            fmt = args.format
        elif ext != '':
            fmt = ext.strip('.')
        else:
            fmt = default_format
        if ext == '':
            outpath += '.'+fmt
        dot.write(outpath, format=fmt)
    else:
        if not args.format:
            args.format = default_format
        suffix = '.%s' % args.format
        if args.format == 'pdf':
            viewer = ['evince', '-w']
        elif args.format == 'svg':
            viewer = ['inkview']
            #viewer = ['iceweasel']
        elif args.format == 'dot':
            viewer = ['xdot']
        else:
            viewer = ['xdg-open']
        try:
            # FIXME: better temp file name
            with tempfile.NamedTemporaryFile(suffix=suffix, delete=False) as f:
                dot.write(f.name, format=args.format)
                try:
                    subprocess.run(viewer + [f.name])
                except OSError as e:
                    sys.exit("Graph viewer '%s' could not be found." % (viewer[0]))
        except KeyboardInterrupt:
            sys.exit()

p = gen_subparser("graph", draw_graph)
p.add_argument('-i', '--index', action='store_true',
               help="add state indices to labels")
p.add_argument('-g', '--gotos', action='store_true',
               help="draw 'goto' edges")
p.add_argument('-j', '--jumps', action='store_true',
               help="draw 'jump' edges")
p.add_argument('-a', '--all', action='store_true',
               help="show all edges (-gj)")
p.add_argument('-c', '--constraints', action='store_true',
               help="use edge constraints for goto and jump edges")
p.add_argument('-f', '--format', metavar='<type>', type=str,
               help="drawing format: 'pdf', 'svg', etc. (default: 'pdf')")
p.add_argument('-o', '--outfile', metavar='<path>', type=str, nargs='?', default=False,
               help="save graph to file.  If path is a directory, a file named \"<system name>.<format>\" will be saved in that directory.  If path includes extension the extension will be used to determine format.")
p.add_argument('state', metavar='<state>', type=str, nargs='?',
               help="state to highlight")
p.add_argument('request', metavar='<request>', type=str, nargs='?',
               help="final (request) state for highlighting path")


def print_files(args):
    """Print paths to all usercode source files."""
    system = cli.init_system(args, load=True)
    print(system.path)
    for path in system.usercode:
        print(path)

p = gen_subparser("files", print_files)


def print_source(args):
    """Print usercode source lines for system object."""
    import inspect
    system = cli.init_system(args, load=True)
    try:
        obj = system.module.__dict__[args.object]
    except KeyError as e:
        sys.exit("Unknown system object: %s" % e)
    sf = inspect.getsourcefile(obj)
    lines, sl = inspect.getsourcelines(obj)
    el = sl + len(lines) - 1
    nwidth = len(str(sl + len(lines)))
    if not args.raw:
        print("object: %s" % (obj))
        print("  file: %s" % (sf))
        print(" lines: %d-%d" % (sl, el))
        print('-' * (nwidth + len(lines[0])))
    prefix = ''
    for line in lines:
        if not args.raw:
            prefix = \
                colored('{sl:{nwidth}}'.format(sl=sl, nwidth=nwidth), 'green') + \
                colored(':', 'cyan')
            sl += 1
        print('%s%s' % (prefix, line), end='')

p = gen_subparser("source", print_source)
p.add_argument('object', metavar='<object>',
               help="code object")
p.add_argument('-r', '--raw', action='store_true',
               help="raw source, without header or line numbers")


def edit_system(args):
    """Edit system usercode."""
    try:
        system = cli.init_system(args, load=True)
    except:
        print("WARNING: Exception encountered when loading module:\n", file=sys.stderr)
        print(traceback.format_exc(), file=sys.stderr)
        system = cli.init_system(args, load=False)
    cmd = []
    if args.editor:
        cmd = args.editor.split()
    elif os.getenv('EDITOR'):
        cmd = os.getenv('EDITOR').split()
    else:
        # try to find XDG-specified handler for text/x-python
        from gi.repository import Gio
        mimetype = 'text/x-python'
        # XDG DATA directories
        # http://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html
        xdg_share = [os.getenv('XDG_DATA_HOME', os.path.expanduser('~/.local/share'))] \
                    + os.getenv('XDG_DATA_DIRS', '/usr/local/share:/usr/share').split(':')
        # get desktop handler for mimetype
        desktop = subprocess.check_output(
            ['xdg-mime', 'query', 'default', mimetype],
            universal_newlines=True,
        ).strip()
        # look for desktop file
        if desktop:
            for d in xdg_share:
                path = os.path.join(d, 'applications', desktop)
                if not os.path.exists(path):
                    continue
                # extract executable
                app = Gio.DesktopAppInfo.new_from_filename(path)
                exe = app.get_executable()
                if exe:
                    cmd = [exe]
                    break
    if not cmd:
        cmd = ['emacs']
    if args.object:
        import inspect
        try:
            obj = system.module.__dict__[args.object]
        except KeyError as e:
            sys.exit("Unknown system object: %s" % e)
        source_file = inspect.getsourcefile(obj)
        source_lines, line_start = inspect.getsourcelines(obj)
        line_end = line_start + len(source_lines)
        #cmd += ['+%d' % sl, sf]
        if 'emacs' in cmd[0]:
            cmd += [source_file,
                    '--eval',
                    "(narrow-to-region (line-beginning-position {}) (line-end-position {}))".format(line_start, line_end),
                    ]
        elif 'gedit' in cmd[0]:
            cmd += [source_file, '+{}'.format(line_start)]
        else:
            sys.exit("Object editing only supported with emacs and gedit.")
    else:
        cmd += [system.path]
        if system.usercode:
            cmd += system.usercode
    print(' '.join(cmd), file=sys.stderr)
    try:
        try:
            subprocess.run(cmd)
        except OSError:
            sys.exit("Could not find editor: %s" % cmd[0])
    except KeyboardInterrupt:
        sys.exit()

p = gen_subparser("edit", edit_system)
p.add_argument('object', metavar='<object>', type=str, nargs='?',
               help="object code to jump to in editor")
p.add_argument('-e', '--editor', type=str,
               help="editor (emacs, gedit, etc. (default: xdg-open))")


def archive_clone(args):
    """Clone system usercode git archive for inspection.

The GUARD_ARCHIVE_ROOT environment variable points to the root
directory where all system archives are stored.  This may point to at
network location as well (e.g. https://...)."""
    if not ARCHIVE_ROOT:
        sys.exit("GUARD_ARCHIVE_ROOT or IFO/SITE env vars must be specified to find code archives.")

    import git

    archive_path = os.path.join(ARCHIVE_ROOT, args.system)

    if '://' not in archive_path:
        archive_path = os.path.abspath(archive_path)

    tempdir = None
    if args.directory:
        if not os.path.exists(args.directory):
            os.mkdir(args.directory)
        git.Git(args.directory).clone(archive_path)
    else:
        tempdir = tempfile.mkdtemp(prefix='guardutil_archive_%s_' % (args.system))
        try:
            wd = os.path.join(tempdir, args.system)
            os.mkdir(wd)
            #git.Repo.clone_from(archive_path, root)
            git.Git(tempdir).clone(archive_path)
            os.chdir(wd)
            if args.shell:
                subprocess.run('bash')
            else:
                subprocess.run('gitk')
        except KeyboardInterrupt:
            pass
        finally:
            if tempdir:
                shutil.rmtree(tempdir)

p = gen_subparser("archive-clone", archive_clone)
# requires name only (not path) so create separately
p.add_argument('directory', metavar='<directory>', nargs='?',
               help="checkout directory")
p.add_argument('-s', '--shell', action='store_true',
               help="open shell in checked-out repo")


def print_history(args):
    """Print state history around specified time (NDS)."""
    from gpstime import gpstime
    system = cli.init_system(args, load=True)
    channel = const.CAS_PREFIX_FMT.format(IFO=system.ifo, SYSTEM=system.name) + 'STATE_N'
    time0 = gpstime.parse(args.time0)
    if args.time1:
        t0 = int(time0.gps())
        t1 = int(gpstime.parse(args.time1).gps())
    else:
        window = eval(args.window)
        t0 = int(time0.gps() + window[0])
        t1 = int(time0.gps() + window[1])
    print("NDS fetch: {} -> {}".format(t0, t1), file=sys.stderr)
    conn = nds_connection()
    buf = conn.fetch(t0, t1, [channel])
    buf = buf[0]
    state_array = buf.data

    # some helper function
    def i2t(i):
        """gps time of NDS data index"""
        return buf.gps_seconds + i*(1./buf.channel.sample_rate)
    def stime(gt):
        """gpstime string"""
        if args.tfmt == 'gps':
            return '{:.6f}'.format(gt.gps())
        elif args.tfmt == 'delta':
            td = gt.gps() - time0.gps()
            return '{:.3f}'.format(td)
        else:
            return gt.iso()
    def ptrans(last, cur):
        """print transition"""
        print('{time} {frm} -> {to}'.format(
            time=stime(cur[1]), frm=last[0], to=cur[0]))
    def pdur(last, cur):
        """print duration"""
        print('{state:{msl}s} {ent} -> {ext} ({dur})'.format(
            msl=msl, state=last[0],
            ent=stime(last[1]), ext=stime(cur[1]), dur=(cur[1] - last[1])))

    # find all transitions
    trans = []
    last = None
    for i, s in enumerate(state_array):
        if s == last:
            continue
        trans.append((
            system.index(int(s)),
            gpstime.fromgps(i2t(i)),
            ))
        last = s
    # record the last sample
    trans.append((
        system.index(int(s)),
        gpstime.fromgps(i2t(i)),
        ))
    msl = max([len(s) for s,t in trans])
    # print the results
    last = trans[0]
    for cur in trans[:-1]:
        if cur == last:
            continue
        if args.transitions:
            ptrans(last, cur)
        else:
            pdur(last, cur)
        last = cur
    if not args.transitions:
        pdur(last, trans[-1])

p = gen_subparser("print-hist", print_history)
p.add_argument('-t', '--transitions', action='store_true',
               help="show state transitions rather than duration")
p.add_argument('-g', '--gps', action='store_const', dest='tfmt', const='delta',
               help="show times in GPS")
p.add_argument('-d', '--delta', action='store_const', dest='tfmt', const='delta',
               help="show times as delta relative to time0")
p.add_argument('-w', '--window', metavar='<window>', default='[-60, 60]',
               help="time window if time1 not specified (default: [-60, 60])")
p.add_argument('time0', metavar='<time0>',
               help="center time, or start time if time1 specified")
p.add_argument('time1', metavar='<time1>', nargs='?',
               help="stop time")


def plot_history(args):
    """Plot node data/state around specified time (NDS)."""
    import guardian.plotutils as pu
    system = cli.init_system(args, load=True)
    center_time = args.time0
    window = eval(args.window)
    pu.guardfig(system, center_time, window)

p = gen_subparser("plot-hist", plot_history)
p.add_argument('-w', '--window', metavar='<window>', default='[-60, 60]',
               help="time window if time1 not specified (default: [-60, 60])")
p.add_argument('time0', metavar='<time0>',
               help="center or start time")
p.add_argument('time1', metavar='<time1>', nargs='?',
               help="duration or stop time")

############################################################

def main():
    args = parser.parse_args()
    # try:
    #     system = cli.init_system(args, load=True)
    # except:
    #     print("WARNING: Exception encountered when loading module:\n", file=sys.stderr)
    #     print(traceback.format_exc(), file=sys.stderr)
    #     system = cli.init_system(args, load=False)
    # arg_filter = ['cmd', 'func', 'system', 'name']
    # cargs = {k:v for k,v in args.__dict__.items() if k not in arg_filter}
    # print(cargs)
    args.func(args)

if __name__ == '__main__':
    main()
