from sarah.lazy import lazy

@lazy
def yaml():
    import yaml
    return yaml

import os
import sys
import copy
import errno
import os.path
import msgpack
import logging

from itertools import chain

from sarah.order import sort_by_successors, dual_cover, restricted_cover

from mirte.core import ModuleDefinition, DepDefinition, VSettingDefinition

CACHE_FILENAME_TEMPLATE = '.%s.msgpack'
FILE_SUFFIX = '.mirte'
DEFAULT_FILE = 'default.mirte'


def depsOf_of_mirteFile_instance_definition(man, insts):
    """ Returns a function that returns the dependencies of
        an instance definition by its name, where insts is a
        dictionary of instance definitions from a mirteFile """
    return lambda x: map(lambda a: a[1],
                 filter(lambda b: b[0] in \
                [dn for dn, d in (
                    man.modules[
                        insts[x]['module']
                        ].deps.iteritems()
                    if 'module' in insts[x] else [])],
                insts[x].items()))

def depsOf_of_mirteFile_module_definition(defs):
    """ Returns a function that returns the dependencies of a module
        definition by its name, where defs is a dictionary of module
        definitions from a mirteFile """
    return lambda x: (filter(lambda z: not z is None and z in defs,
                 map(lambda y: y[1].get('type'),
                     defs[x]['settings'].items()
                    if 'settings' in defs[x] else []))) + \
             (list(defs[x]['inherits']) if 'inherits' in defs[x] else [])

def module_definition_from_mirteFile_dict(man, d):
    """ Creates a ModuleDefinition instance from the dictionary <d> from
        a mirte-file for the Manager instance <man>. """
    m = ModuleDefinition()
    if not 'inherits' in d: d['inherits'] = list()
    if not 'settings' in d: d['settings'] = dict()
    if 'implementedBy' in d:
        m.implementedBy = d['implementedBy']
    m.inherits = set(d['inherits'])
    for p in d['inherits']:
        if not p in man.modules:
            raise ValueError, "No such module %s" % p
        m.deps.update(man.modules[p].deps)
        m.vsettings.update(man.modules[p].vsettings)
        m.inherits.update(man.modules[p].inherits)
        m.run = m.run or man.modules[p].run
    if 'run' in d:
        m.run = d['run']
    if len(m.inherits) == 0:
        m.inherits = set(['module'])
    for k, v in d['settings'].iteritems():
        if not 'type' in v:
            if not k in m.vsettings:
                raise ValueError, \
                    "No such existing vsetting %s" % k
            if 'default' in v:
                m.vsettings[k] = copy.copy(m.vsettings[k])
                m.vsettings[k].default = v['default']
            continue
        if v['type'] in man.modules:
            m.deps[k] = DepDefinition(v['type'], v.get('allownull', False))
        elif v['type'] in man.valueTypes:
            m.vsettings[k] = VSettingDefinition(v['type'],
                (man.valueTypes[v['type']](v['default'])
                    if 'default' in v else None))
        else:
            raise ValueError, \
                "No such module or valuetype %s" % v
    return m

def load_mirteFile(path, m, logger=None):
    """ Loads the mirte-file at <path> into the manager <m>. """
    l = logging.getLogger('load_mirteFile') if logger is None else logger
    had = set()
    for name, path, d in walk_mirteFiles(path, logger):
        if os.path.realpath(path) in m.loaded_mirteFiles:
            continue
        identifier = name
        if name in had:
            identifier = path
        else:
            had.add(name)
        l.info('loading %s' % identifier)
        m.loaded_mirteFiles.add(os.path.realpath(path))
        _load_mirteFile(d, m)

def _load_mirteFile(d, m):
    """ Loads the dictionary from the mirteFile into <m> """
    defs = d['definitions'] if 'definitions' in d else {}
    insts = d['instances'] if 'instances' in d else {}
    # Filter out existing instances
    insts_to_skip = []
    for k in insts:
        if k in m.insts:
            m.update_instance(k, dict(insts[k]))
            insts_to_skip.append(k)
    for k in insts_to_skip:
        del(insts[k])
    # Sort module definitions by dependency
    it = sort_by_successors(defs.keys(), dual_cover(defs.keys(),
        restricted_cover(defs.keys(),
                 depsOf_of_mirteFile_module_definition(defs))))
    # Add module definitions
    for k in it:
        m.add_module_definition(k,
            module_definition_from_mirteFile_dict(m, defs[k]))
    # Sort instance declarations by dependency
    it = sort_by_successors(insts.keys(),
        dual_cover(insts.keys(), restricted_cover(insts.keys(),
            depsOf_of_mirteFile_instance_definition(m, insts))))
    # Create instances
    for k in it:
        settings = dict(insts[k])
        del(settings['module'])
        m.create_instance(k, insts[k]['module'], settings)

def find_mirteFile(name, extra_path=None):
    """ Resolves <name> to a path.  Uses <extra_path> """
    extra_path = () if extra_path is None else extra_path
    for bp in chain(extra_path, sys.path):
        pb = os.path.join(bp, name)
        p = pb + FILE_SUFFIX
        if os.path.exists(p):
            return os.path.abspath(p)
        p = os.path.join(pb, DEFAULT_FILE)
        if os.path.exists(p):
            return os.path.abspath(p)
    raise ValueError, "Couldn't find mirteFile %s" % name

def walk_mirteFiles(name, logger=None):
    """ Yields (cpath, d) for all dependencies of and including the
        mirte-file <name>, where <d> are the dictionaries from
        the mirte-file at <cpath> """
    stack = [(name, find_mirteFile(name, (os.getcwd(),)))]
    loadStack = []
    had = dict()
    while stack:
        name, path = stack.pop()
        if path in had:
            d = had[path]
        else:
            d = _parse_mirteFile(path, logger)
            had[path] = d
        loadStack.append((name, path, d))
        if not 'includes' in d:
            continue
        for include in d['includes']:
            stack.append((include, find_mirteFile(include,
                (os.path.dirname(path),))))
    had = set()
    for name, path, d in reversed(loadStack):
        if path in had:
            continue
        had.add(path)
        yield name, path, d

def _parse_mirteFile(path, logger=None):
    """ Open and parses the mirteFile at <path>. """
    l = logging.getLogger('_parse_mirteFile') if logger is None else logger
    cache_path = os.path.join(os.path.dirname(path),
                CACHE_FILENAME_TEMPLATE % os.path.basename(path))
    if (os.path.exists(cache_path) and
                os.path.getmtime(cache_path) >= os.path.getmtime(path)):
        with open(cache_path) as f:
            return msgpack.unpack(f)
    with open(path) as f:
        ret = yaml.load(f)
    try:
        with open(cache_path, 'w') as f:
            msgpack.pack(ret, f)
    except IOError as e:
        if e.errno == errno.EACCES:
            l.warn('Not allowed to write %s', path)
        else:
            raise
    return ret

# vim: et:sta:bs=2:sw=4:
