#!/usr/bin/python
# -*- 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 optparse
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='mididings %s, using Python %s' %
                        (mididings.__version__, 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__':
    usage   = "Usage: mididings [backend options] \"patch\"\n" \
              "       mididings [backend options] [mode option]"
    description = "A MIDI router and processor based on Python."
    epilog  = "See the mididings manual for more information."
    version = ("mididings %s, using Python %s" %
                        (mididings.__version__, platform.python_version()))

    parser = optparse.OptionParser(usage=usage, description=description,
                                   epilog=epilog, version=version)

#    parser.format_description = lambda formatter: parser.description
#    parser.format_epilog = lambda formatter: parser.epilog

    backopt = optparse.OptionGroup(parser, "Backend options")
    backopt.add_option('-b', dest='backend', help=optparse.SUPPRESS_HELP)
    backopt.add_option('-A', dest='backend',
                       action='store_const', const='alsa',
                       help="use ALSA sequencer")
    backopt.add_option('-J', dest='backend',
                       action='store_const', const='jack',
                       help="use JACK MIDI (buffered)")
    backopt.add_option('-R', dest='backend',
                       action='store_const', const='jack-rt',
                       help="use JACK MIDI (realtime)")
    backopt.add_option('-c', dest='client_name',
                       help="ALSA or JACK client name")
    backopt.add_option('-i', dest='in_ports', type=int,
                       help="number of input ports")
    backopt.add_option('-o', dest='out_ports', type=int,
                       help="number of output ports")
    backopt.add_option('-I', dest='in_connections', type=str, action='append',
                       help='input port connections (regular expression)')
    backopt.add_option('-O', dest='out_connections', type=str, action='append',
                       help='output port connections (regular expression)')
    parser.add_option_group(backopt)

    gnrlopt = optparse.OptionGroup(parser, "General options")
    gnrlopt.add_option('-d', dest='start_delay', type=float,
                       help="delay (in seconds) before processing starts")
    gnrlopt.add_option('-t', dest='octave_offset', type=int,
                       help="octave offset")
    gnrlopt.add_option('-z', dest='data_offset', type=int,
                       help="offset for port, channel, and program numbers")
    parser.add_option_group(gnrlopt)

    modeopt = optparse.OptionGroup(parser, "Mode options")
    modeopt.add_option('-f', dest='filename',
                       help="file name of script to run")
    modeopt.add_option('-s', dest='interactive', action='store_true',
                       help="interactive shell")
    modeopt.add_option('-S', dest='interactive_auto', action='store_true',
                       help="interactive shell with automatic patch execution")
    modeopt.add_option('-p', action='store_true', dest='print_events',
                       help="print MIDI events (no processing)")
    parser.add_option_group(modeopt)

    options, args = parser.parse_args(sys.argv[1:])

    if (len(args) == 0 and not options.filename and
            not options.print_events and
            not options.interactive and
            not options.interactive_auto):
        parser.error("no patch and no file name specified")
    elif len(args) > 1:
        parser.error("more than one patch specified")
    elif options.filename and (options.interactive or
                               options.interactive_auto):
        parser.error("file name and interactive shell are mutually exclusive")

    app = Dings(options)

    if options.print_events:
        app.run_print()
    elif options.filename:
        app.run_file(options.filename)
    elif options.interactive:
        app.run_interactive(args[0] if len(args) else None, False)
    elif options.interactive_auto:
        app.run_interactive(args[0] if len(args) else None, True)
    else:
        app.run_patch(args[0])
