#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# mididings
#
# Copyright (C) 2008-2014  Dominic Sacré  <dominic.sacre@gmx.de>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#

import sys
import os
import argparse
import platform
import functools

import mididings
import mididings.extra
import mididings.setup
import mididings.engine
import mididings.patch


try:
    import xdg.BaseDirectory

    MIDIDINGS_CONFIG_DIR = os.path.join(xdg.BaseDirectory.xdg_config_home, "mididings")
    MIDIDINGS_DATA_DIR = os.path.join(xdg.BaseDirectory.xdg_data_home, "mididings")
except ImportError:
    MIDIDINGS_CONFIG_DIR = os.path.expanduser("~/.config/mididings")
    MIDIDINGS_DATA_DIR = os.path.expanduser("~/.local/share/mididings")

MIDIDINGS_DEFAULTS = os.path.join(MIDIDINGS_CONFIG_DIR, "default.py")
MIDIDINGS_HISTFILE = os.path.join(MIDIDINGS_DATA_DIR, "history")
MIDIDINGS_HISTSIZE = 100


class Dings(object):
    def __init__(self, options):
        # build dict of all public members of mididings and mididings.extra
        self.dings_dict = dict(
            (k, v) for k, v in mididings.__dict__.items() if k in mididings.__all__
        )
        self.dings_dict.update(
            dict(
                (k, v)
                for k, v in mididings.extra.__dict__.items()
                if k in mididings.extra.__all__
            )
        )

        # load defaults from config file
        try:
            self.exec_dings(MIDIDINGS_DEFAULTS)
        except IOError:
            pass

        # handle command line options
        if options.backend:
            self.config(backend=options.backend)
        if options.client_name:
            self.config(client_name=options.client_name)
        if options.start_delay is not None:
            self.config(start_delay=options.start_delay)
        if options.data_offset is not None:
            self.config(data_offset=options.data_offset)
        if options.octave_offset is not None:
            self.config(octave_offset=options.octave_offset)
        if options.in_ports is not None or options.in_connections:
            in_ports = self.make_port_definitions(
                options.in_connections, options.in_ports
            )
            self.config(in_ports=in_ports)
        if options.out_ports is not None or options.out_connections:
            out_ports = self.make_port_definitions(
                options.out_connections, options.out_ports
            )
            self.config(out_ports=out_ports)

    def config(self, **kwargs):
        mididings.setup._config_impl(override=True, **kwargs)

    def make_port_definitions(self, connections, nports):
        if connections is None:
            return nports
        elif nports is None:
            pass
        elif nports < len(connections):
            connections = connections[:nports]
        elif nports > len(connections):
            connections += [None] * (nports - len(connections))

        return [(None, c) for c in connections]

    def exec_dings(self, filename):
        exec(compile(open(filename).read(), filename, "exec"), self.dings_dict)

    def run_file(self, filename):
        # add filename's directory to sys.path to allow import from the same
        # directory
        d = os.path.dirname(filename)
        if not d:
            d = "."
        sys.path.insert(0, d)
        # just a kludge to make AutoRestart() work
        sys.modules["__mididings_main__"] = type(
            "MididingsMain", (), {"__file__": os.path.abspath(filename)}
        )

        self.exec_dings(filename)

        # avoid memory leaks
        self.dings_dict.clear()

    def run_patch(self, patch):
        mididings.run(eval(patch, self.dings_dict))

    def run_print(self):
        # don't override user-defined client name
        mididings.setup.config(client_name="printdings")
        self.config(out_ports=0)
        mididings.run(mididings.Print())

    def run_interactive(self, source=None, auto=False):
        import code
        import readline  # it's... magic!

        shell = code.InteractiveConsole(self.dings_dict)
        readline.set_history_length(MIDIDINGS_HISTSIZE)
        try:
            readline.read_history_file(MIDIDINGS_HISTFILE)
        except IOError:
            pass

        # set up simple tab completion
        def complete_dings(text, state):
            for cmd in self.dings_dict.keys():
                if cmd.startswith(text):
                    if not state:
                        return cmd
                    else:
                        state -= 1

        readline.parse_and_bind("tab: complete")
        readline.set_completer(complete_dings)

        # execute source from command line
        if source:
            shell.runsource(source)

        # start backend (and leave it running until mididings exits)
        mididings.engine._start_backend()

        # print newline after run()
        @functools.wraps(mididings.run)
        def run_wrapper(*args, **kwargs):
            mididings.run(*args, **kwargs)
            print("")

        self.dings_dict["run"] = run_wrapper

        if auto:
            # monkey-patch shell.raw_input() to remember whether the input
            # started with a blank.
            started_with_blank = [False]

            def raw_input_wrapper(prompt):
                r = code.InteractiveConsole.raw_input(shell, prompt)
                started_with_blank[0] = r.startswith(" ")
                if started_with_blank[0]:
                    return r[1:]
                else:
                    return r

            shell.raw_input = raw_input_wrapper

            # replace sys.displayhook to execute anything that is a valid
            # mididings patch, unless input started with a blank.
            displayhook_orig = sys.displayhook

            def displayhook_wrapper(value):
                try:
                    # build a throw-away patch to check for exceptions
                    mididings.patch.Patch(value)
                    is_patch = True
                except TypeError:
                    is_patch = False

                if is_patch and not started_with_blank[0]:
                    run_wrapper(value)
                else:
                    displayhook_orig(value)

            sys.displayhook = displayhook_wrapper

        # run interactive shell
        shell.interact(
            banner=f"mididings {mididings.__version__}, using Python {platform.python_version()}"
        )
        # save history
        try:
            os.makedirs(MIDIDINGS_DATA_DIR)
        except OSError:
            pass
        try:
            readline.write_history_file(MIDIDINGS_HISTFILE)
        except IOError:
            pass


if __name__ == "__main__":
    parser = argparse.ArgumentParser(
        prog="mididings",
        #    usage="""
        #    Usage: mididings [backend options] \"patch\"
        #           mididings [backend options] [mode option]""",
        description="A MIDI router and processor.",
        epilog="See the mididings manual for more information.",
    )

    backend_group = parser.add_argument_group("Backend options")
    backend_mutex = backend_group.add_mutually_exclusive_group()
    general_group = parser.add_argument_group("General options")
    mode_group = parser.add_argument_group("Mode options")

    backend_mutex.add_argument(
        "-A",
        dest="backend",
        action="store_const",
        const="alsa",
        help="use ALSA sequencer",
    )
    backend_mutex.add_argument(
        "-J",
        dest="backend",
        action="store_const",
        const="jack",
        help="use JACK MIDI (buffered)",
    )
    backend_mutex.add_argument(
        "-R",
        dest="backend",
        action="store_const",
        const="jack-rt",
        help="use JACK MIDI (realtime)",
    )
    backend_group.add_argument("-c", dest="client_name", help="ALSA/JACK client name")
    backend_group.add_argument(
        "-i", dest="in_ports", type=int, help="number of input ports"
    )
    backend_group.add_argument(
        "-o", dest="out_ports", type=int, help="number of output ports"
    )
    backend_group.add_argument(
        "-I",
        dest="in_connections",
        action="append",
        type=str,
        help="input port connections (regular expression)",
    )
    backend_group.add_argument(
        "-O",
        dest="out_connections",
        action="append",
        type=str,
        help="output port connections (regular expression)",
    )

    general_group.add_argument(
        "-d",
        dest="start_delay",
        type=float,
        help="delay (seconds) before processing starts",
    )
    general_group.add_argument(
        "-t", dest="octave_offset", type=int, help="octave offset"
    )
    general_group.add_argument(
        "-z",
        dest="data_offset",
        type=int,
        help="offset for port, channel & program numbers",
    )

    mode_group.add_argument("-f", dest="filename", help="file name of script to run")
    mode_group.add_argument(
        "-s", dest="interactive", action="store_true", help="interactive shell"
    )
    mode_group.add_argument(
        "-S",
        dest="interactive_auto",
        action="store_true",
        help="interactive shell with automatic patch execution",
    )
    mode_group.add_argument(
        "-p",
        dest="print_events",
        action="store_true",
        help="print MIDI events (no processing)",
    )

    parser.add_argument(
        "--version",
        action="version",
        version=f"%(prog)s {mididings.__version__}, using Python {platform.python_version()}",
    )

    parser.add_argument("patch", nargs="?", help="name of patch")

    args = parser.parse_args()

    if (
        not args.patch
        and not args.filename
        and not args.print_events
        and not args.interactive
        and not args.interactive_auto
    ):
        parser.print_usage()
        print("No patch and no filename specified.")
        sys.exit(1)
    elif args.filename and (args.interactive or args.interactive_auto):
        print("File name and interactive shell are mutually exclusive.")
        sys.exit(1)

    app = Dings(args)

    if args.print_events:
        app.run_print()
    elif args.filename:
        app.run_file(args.filename)
    elif args.interactive:
        app.run_interactive(args.patch if args.patch else None, False)
    elif args.interactive_auto:
        app.run_interactive(args.patch if args.patch else None, True)
    else:
        app.run_patch(args.patch)
