 
"""Handle commandline options to produce IterNode objects"""


from argparse import ArgumentParser, Action
from collections import OrderedDict
from .phlasses import PhraseMaker, SeqGen, IterNode
from . import seqmods
         
class GetRange(Action):
    "Override Action.__call__."
    def __call__(self, parser, namespace, values, option_string=None):
        "Process options which take a single value or a range pair  (x-y)"
        value = []
        try:
            for string in values:
                if '-' in string:
                    dash = string.index("-")
                    elt = sorted([abs(int(string[:dash])),
                    abs(int(string[dash + 1:])) ])
                else:
                    elt = [abs(int(string))] * 2
                value.append(elt)
            setattr(namespace, self.dest, value)
        except(IndexError, ValueError):
            print(("\n", option_string, " needs numbers or ranges (x-y)."))

class GetValRange(Action):
    "Override Action.__call__."
    def __call__(self, parser, namespace, values, option_string=None):
        """Process options which take a series of parameters
        with single or range (x-y) values"""
        value = []
        try:
            for i, string in enumerate(values):
                if not i % 2:
                    value.append(abs(int(string)))
                else:
                    if '-' in string:
                        dash = string.index("-")
                        value += sorted([ abs(int(string[ : dash ])),
                        abs(int(string[  dash+1 : ])) ])
                    else:
                        value += [ abs(int(string)) ] * 2
            current_val = getattr(namespace, self.dest, None)
            if current_val:
                current_val.append(value)
            else:
                value = [value]
                setattr(namespace, self.dest, value)
        except(IndexError, ValueError):
            print(("\n", option_string, (" needs parameter numbers " +
                "followed by single or range (x-y) values.")))

class BoolDefault(Action):
    "Override Action.__call__."
    def __call__(self, parser, namespace, values, option_string=None):
        "Give boolean options a default of [1] if called without arguments"
        try:
            value = [abs(int(i)) for i in values] or [1]
            setattr(namespace, self.dest, value)
        except(ValueError):
            print(("\n", option_string, " takes numbers."))
      

def autoparser(*modules):
    "Create argument parser from classes or imported modules."
    parser = ArgumentParser()
    for module in modules:
        group = parser.add_argument_group(module.__name__)
        for name, func in vars(module).items():
            if name[0] != '_':
                doc = func.__doc__
                if '(x-y)' in doc:
                    argdict = {'action':GetRange, 'nargs':'+'}
                elif '(x y-z)' in doc:
                    argdict = {'action':GetValRange, 'nargs':'+'}
                elif 'Boolean' in doc:
                    argdict = {'action':BoolDefault, 'type':int, 'nargs':'*'}
                elif 'Repeatable' in doc:
                    argdict = {'type':int, 'action':'append', 'nargs':'+'}
                else:
                    argdict = {'type':int, 'nargs':'+'}
                group.add_argument('--' + name, help=doc, **argdict)
    return parser

    
def optparser(optlist):
    """Process commandline options; return as dictionaries of option groups"""
    parser = ArgumentParser(description = __doc__)
    parser.add_argument('name', nargs='?', help="A name for the instrument")
                
    work_opts = parser.add_argument_group('General')
    work_opts.add_argument("--qsize",  type=int, default=1,
        help="How many phrases to prepare ahead of time.")
    work_opts.add_argument("--display", type=int, nargs='?', const=16,
        help="Create PDF score file in home directory" +
        "refreshing each 16 phrases, or number entered")
               
    score_opts = parser.add_argument_group('Score')
    score_opts.add_argument("-L", "--score",
        choices=('normal', 'percussion'), nargs='?',
        const='normal', help="Create PDF score in home directory")  
    score_opts.add_argument("--midi", action="store_true" ,
        help="Create midi file in home directory")

    play_opts = parser.add_argument_group('Playback')
    group = play_opts.add_mutually_exclusive_group()
    group.add_argument('-F', '--fluidsynth',
        help="Path to soundfont for fluidsynth playback",
        nargs='?', const='/usr/share/sounds/sf2/FluidR3_GM.sf2', type=open)
    play_opts.add_argument("--channel", help="MIDI channel.",
        type=int, default=0)
    play_opts.add_argument("--bank",  help="MIDI bank", default='0')
    play_opts.add_argument("--font", help="Sounfont I.D. number", default='1')
    play_opts.add_argument("--driver", help="Driver for fluidsynth",
        default='alsa')    
    group.add_argument('-S', '--samples', help=("""Path to a directory from
        which samples will be recursively collected once before playback"""))
    group.add_argument('-D', '--directory',
        help="""Path to a modifyable directory of samples
        which may be modified during playback""")
    group.add_argument('-P', '--sox',
        help="Sox synth playback, choose type from sine or pinknoise",
                choices=('sine', 'pinknoise'), nargs='?', const='sine')
    play_opts.add_argument("--orchestra", action="store_true",
        help="Show list of avaiable instruments")
        
    opts = parser.parse_args(optlist)
    
    groupdicts = [{i.dest:getattr(opts, i.dest)
                    for i in group._group_actions}
                    for group in parser._action_groups[2:]]

    return opts.name, [{i:j for i, j in d.items() if j} for d in groupdicts]

    
def findopt(string, parser):
    "Find unique full name of abbreviated options"
    results = [i.dest for i in parser._actions if i.dest.startswith(string)]
    if len(results) == 1:
        if results == ['help']:
            parser.parse_args(['--help'])
        return results[0]
    if results:
        print("    Ambiguous option: " + string + " could be: " +
        ' '.join([i for i in results]))
    else:
        print("    No such option: " + string)
    #raise this to be consistent with argparse
    raise SystemExit   


def generalize(genfunc):
    """Produce a decorator to modify output of
        number-list-producing iterator according
        to behaviour of a list-taking
        generator function"""
    def wrapper(listgen):
        "As above"
        def newgen(*args):
            "As above"
            for lis in listgen(*args):
                for j in genfunc(lis):
                    yield j 
        newgen.__name__ = listgen.__name__ + '-' + genfunc.__name__
        return newgen
    return wrapper
       
@generalize    
def elt_gen(nlist):
    """Generate one element at a time from a list"""
    for number in nlist:
        yield number

        
@generalize 
def step_elt_gen(nlist):
    """Generate the intervals between list elements, one at a time."""
    if hasattr(nlist, 'full_steps'):
        nlist = nlist.full_steps()
    for number in nlist:
        yield number

        
@generalize 
def range_gen(nlist):
    """Generate lists of two elements from a list"""
    for i in range(len(nlist) // 2):
        yield sorted(nlist.pitches[2 * i:2 * i + 2])

        
@generalize
def steplist_gen(nlist):
    """Generate a list of the intervals between sequence elements"""
    if hasattr(nlist, 'full_steps'): #This is ugly! 
        nlist = nlist.full_steps()    #But lets us use Sequence objects
    yield nlist    

    
def dec_dispatch(caller):
    """Select the decorator required by the caller"""
    doc = caller.__doc__
    if '(x-y)' in doc:
        return range_gen
    if 'Step values' in doc:
        return steplist_gen        
    if 'Boolean' in doc:
        return step_elt_gen
    if 'Repeatable' not in doc:    
        return elt_gen
        
def cycle(args): 
    "Repeatedy iterate"
    while 1:
        for i in args:
            yield i
   
                            
def itertree(optstring, rootns=(PhraseMaker,), subns=(SeqGen, seqmods),
                leaf_iter=cycle, parserfunc=autoparser, parser=None,
                decfunc=dec_dispatch, dec=None, root=True):
    "Create a tree of IterNodes from a bracket-nested comandline string"
    
    if root:
        parser = parser or parserfunc(*rootns)
        subparser = parserfunc(*subns)
        ns_tup = rootns
    else:
        parser = subparser = parser or parserfunc(*subns)
        ns_tup = subns            
            
    funcns = {}
    for d in ns_tup:
       funcns.update(vars(d))
    iterator = ns_tup[0]
    if dec:
        iterator = dec(iterator)
          
    topopts, parsable, subopts = [], [], []
    optdict = {}
    count = 0
    currentarg = None
    optlist = optstring.replace('[', ' [ ').replace(']', ' ] ').split()
    for string in optlist:
        if count:            
            count += string == '['
            count -= string == ']'
            if count:
                subopts.append(string) 
            else:
                optdict[topopts[-1]] = itertree(' '.join(subopts),
                                        parser=subparser,
                                         dec=decfunc(funcns[topopts[-1]]),
                                        root=False)
        else:
            if string == '[':
                if not currentarg:
                    print("\nNo option specified\n")
                    raise SystemExit
                subopts = []
                parsable.pop()
                count = 1                                  
            else:
                try:
                    int(string.replace('-', ''))
                    currentarg = None
                    parsable.append(string)
                except ValueError:
                    currentarg = findopt(string, parser)
                    topopts.append(currentarg)
                    parsable.append('--' + string)                
    if count:
        print("\ninvalid suboption\n")
        raise SystemExit
    nmspce = parser.parse_args(parsable)

    return  IterNode(iterator, *[OrderedDict(
            (funcns[k], optdict.get(k) or IterNode(leaf_iter, (getattr(nmspce, k))))
            for k in topopts if k in [a.dest for a in group._group_actions])
            for group in parser._action_groups[2:]])
