#!/usr/bin/env python
"""This takes the ``eiscp-commands.yaml`` file, as generated by
``import_protocol_doc.py``, and writes the ``eiscp.commands`` Python module.
"""

import sys
import os
from datetime import datetime
import yaml
from collections import OrderedDict
import yaml.constructor


# Since we use lists as keys, we need to load them as tuples
def construct_tuple(loader, node):
    return tuple(yaml.SafeLoader.construct_sequence(loader, node))
yaml.SafeLoader.add_constructor(u'tag:yaml.org,2002:seq', construct_tuple)

# We also need to load mappings in order
# Based on https://gist.github.com/844388
def construct_ordereddict(loader, node):
    data = OrderedDict()
    yield data
    value = construct_mapping(loader, node)
    data.update(value)

def construct_mapping(self, node, deep=False):
    if isinstance(node, yaml.MappingNode):
        self.flatten_mapping(node)
    else:
        raise yaml.constructor.ConstructorError(
            None, None, 'expected a mapping node, but found %s' % node.id, node.start_mark)

    mapping = OrderedDict()
    for key_node, value_node in node.value:
        key = self.construct_object(key_node, deep=deep)
        try:
            hash(key)
        except TypeError, exc:
            raise yaml.constructor.ConstructorError(
                'while constructing a mapping', node.start_mark,
                'found unacceptable key (%s)' % exc, key_node.start_mark)
        value = self.construct_object(value_node, deep=deep)
        mapping[key] = value
    return mapping

yaml.SafeLoader.add_constructor(u'tag:yaml.org,2002:map', construct_ordereddict)


# Load YAML file
with open(sys.argv[1], 'r') as f:
    data = yaml.safe_load(f)


# Remove modelsets key, not a real zone
zones = data
del zones['modelsets']

# Generate the Python structures
#
# We want a command dict that does not include the model data, which we
# are not using.
COMMANDS = OrderedDict([
    (zone, OrderedDict([
        (command, {
            'name': command_data['name'],
            'description': command_data['description'],
            'values': OrderedDict([
                (value, {
                    'name': value_data.get('name'),
                    'description': value_data['description'],
                })
                for value, value_data in command_data['values'].items()
            ]),
        })
        for command, command_data in commands.iteritems()
    ]))
    for zone, commands in zones.iteritems()
])

# We also want alias mappings to easily allows us to resolve a user command
# to an actual internal command.

# Manually alias the main zone
ZONE_MAPPINGS = {
    # Use main as default zone
    '': 'main',
    None: 'main'
}


def find_command_aliases(command_list):
    for command, data  in command_list.iteritems():
        name = data['name']
        if not hasattr(name, '__iter__'):
            yield name, command
        else:
            for item in name:
                yield item, command
COMMAND_MAPPINGS = {
    zone : {
        alias : command
        for alias, command in find_command_aliases(commands)
    }
    for zone, commands in zones.iteritems()
}


def find_value_aliases(values):
    for value, data  in values.iteritems():
        # A tuple gives a range, like (0,  100) for the volume
        if isinstance(value, tuple):
            key = xrange(*value)
            yield key, value

        # This is a single value with an alias, i.e.
        # key = volume-up, value = UP
        elif 'name' in data:
            key = data['name']
            if not isinstance(key, tuple):
                yield key, value
            else:
                # It's possible that there are multiple aliases
                # for a command
                for item in key:
                    yield item, value
        else:
            # If no alias ('name' key) is set, this indicates that
            # the value is a pattern, like nnnnn
            # We do not support them yet.
            sys.stderr.write('Skipping patterned argument: %s %s\n' % (value, dict(data)))
            continue


VALUE_MAPPINGS = {
    zone : {
        command : {
            alias : value
            for alias, value in find_value_aliases(command_data['values'])
        }
        for command, command_data in commands.items()
    }
    for zone, commands in zones.iteritems()
}


# Output those structures as source code.
import pretty
# Make pretty support OrderedDicts better
def print_ordereddict(obj, p, cycle):
    if cycle:
        return p.text('OrderedDict(...)')
    p.begin_group(1, 'OrderedDict([')
    keys = obj.keys()
    for idx, key in enumerate(keys):
        if idx:
            p.text(',')
            p.breakable()
        p.text('(')
        p.pretty(key)
        p.text(', ')
        p.pretty(obj[key])
        p.text(')')
    p.end_group(1, '])')
pretty._type_pprinters[OrderedDict] = print_ordereddict

print \
"""# Generated
#  by {0}
#  from {1}
#  at {2}

from collections import OrderedDict


COMMANDS = {commands}

ZONE_MAPPINGS = {zone_mappings}

COMMAND_MAPPINGS = {command_mappings}

VALUE_MAPPINGS = {value_mappings}
""".format(
    os.path.basename(sys.argv[0]), os.path.basename(sys.argv[1]), datetime.now(),
    commands=pretty.pretty(COMMANDS),
    zone_mappings=pretty.pretty(ZONE_MAPPINGS),
    command_mappings=pretty.pretty(COMMAND_MAPPINGS),
    value_mappings=pretty.pretty(VALUE_MAPPINGS),
)


