"""
    Library module to handle configuration yaml file.

    Units:
    - units are implemented as objects, where each unit inherits Unit() object
    - in cases yaml library matches 'config_re' from this module, it's than
      passed to module mechanism.
    - regexp extracts value and unit, based on 'unit_config', method is
      executed and unit object is expected
    - resulting object is stored into final dictionary
"""
import yaml
import re
import math
import collections


class Unit(float):
    """Base class for units, makes sure that mathematical operations won't
       change the unit type. Forbids math operations with different unit types
    """

    def __add__(self, other):
        assert isinstance(other, (float, int, self.__class__))
        return self.__class__(float.__add__(self, other))
    
    def __radd__(self, other):
        assert isinstance(other, (float, int, self.__class__))
        return self.__class__(float.__radd__(self, other))

    def __sub__(self, other):
        assert isinstance(other, (float, int, self.__class__))
        return self.__class__(float.__sub__(self, other))
    
    def __rsub__(self, other):
        assert isinstance(other, (float, int, self.__class__))
        return self.__class__(float.__rsub__(self, other))

    def __div__(self, other):
        assert isinstance(other, (float, int, self.__class__))
        return self.__class__(float.__div__(self, other))
    
    def __rdiv__(self, other):
        assert isinstance(other, (float, int, self.__class__))
        return self.__class__(float.__rdiv__(self, other))

    def __floordiv__(self, other):
        assert isinstance(other, (float, int, self.__class__))
        return self.__class__(float.__floordiv__(self, other))
    
    def __rfloordiv__(self, other):
        assert isinstance(other, (float, int, self.__class__))
        return self.__class__(float.__rfloordiv__(self, other))

    def __mul__(self, other):
        assert isinstance(other, (float, int, self.__class__))
        return self.__class__(float.__mul__(self, other))
    
    def __rmul__(self, other):
        assert isinstance(other, (float, int, self.__class__))
        return self.__class__(float.__rmul__(self, other))

    def __str__(self):
        return "{}({})".format(self.__class__.__name__, float(self))

    def __repr__(self):
        return str(self)

    def __neg__(self):
        return self.__class__(-float(self))

    def __abs__(self):
        return self.__class__(abs(float(self)))


class Distance(Unit):
    """ Space distance, stored in meters """

    @classmethod
    def from_mm(cls, val):
        return cls(val / 1000)

    def as_mm(self):
        return float(self * 1000)

    def as_m(self):
        return float(self)


class Angle(Unit):
    """ Angle, stored in radians """

    @classmethod
    def from_degree(cls, val):
        return cls(math.radians(val))

    def as_degrees(self):
        return math.degrees(self)

    def as_radians(self):
        return float(self)


class Current(Unit):
    """ Current stored in amperes """

    @classmethod
    def from_ma(cls, val):
        return cls(val / 1000)

    def as_ma(self):
        return float(self * 1000)

    def as_a(self):
        return float(self)

class Torque(Unit):
    """ torque stored in Nm """

    @classmethod
    def from_kg_cm(cls, val):
        return cls((9.8 * val) / 100)

    def as_n_m(self):
        return float(self)


class AngleVelocity(Unit):
    """ angle velocity stored in radians per second """

    @classmethod
    def from_s_60_degree(cls, val):
        degrees_sec = 60 / val

        return cls(math.radians(degrees_sec))

    def as_radians_s(self):
        return float(self)


class Mass(Unit):
    """ Mass is stored in kg """

    @classmethod
    def from_g(cls, val):
        return cls(val / 1000)

    def as_kg(self):
        return float(self)

unit_config = {
    "mm": Distance.from_mm,
    "m": Distance,
    u"\xb0": Angle.from_degree,
    "rad": Angle,
    "r": Angle,
    "A": Current,
    "mA": Current.from_ma,
    "kg/cm": Torque.from_kg_cm,
    u"sec/60\xb0": AngleVelocity.from_s_60_degree,
    "kg": Mass,
    "g": Mass.from_g
}
""" unit_config dictionary stores unit configuration.
    Mechanism expects all values to be callable.
    When specific unit is matched, it's value is called with
    numerical value as argument.
    Unit object is expected as return value."""

types = filter(lambda x: isinstance(x, type), unit_config.values())
""" extract all actual types in unit_config, used to register yaml callback """

config_re = re.compile(
    u"^\s*([-\d.]+)({})\s*$".format("|".join(unit_config.keys()))
)
""" regex for matching known units in config file """


def cfg_constructor(loader, node):
    """ yaml callback for matched config_re, extracts unit and value.
        Uses unit_config to create Unit object. """

    value = loader.construct_scalar(node)
    res = re.match(config_re, value)

    val, unit = res.group(1), res.group(2)

    val = float(val)

    if unit not in unit_config:
        raise Exception("Found unknown unit: " + str(unit))

    return unit_config[unit](val)


def cfg_representer(dumper, data):
    """ converts Unit object into float """
    # note: what the hell?
    val = float(data)
    value = unicode(repr(val)).lower()
    return dumper.represent_scalar(u'tag:yaml.org,2002:float', value)


# register all known types into yaml library
for value in types:
    if isinstance(value, type):
        yaml.add_representer(value, cfg_representer)

# register constructor and resolver for yaml library
yaml.add_constructor(u'!cfg', cfg_constructor)
yaml.add_implicit_resolver(u'!cfg', config_re)

ParseFormulaRes = collections.namedtuple(
    "PartseFormulaRes", ["val", "replaced_count", "unreplaced_count"]
)


def _parse_val(val, top_cfg):
    """ parser for variables, if formula is found, it tries to resolve it.
        Returns 'ParseFormulaRes' """

    res = re.match("^\s*%(.*)%\s*$", val)
    if res is None:
        return ParseFormulaRes(val=val, replaced_count=0, unreplaced_count=0)

    # extract formula itself
    formula = res.group(1)

    # use 're' to match all names in formula
    vars = re.findall("[a-zA-Z][a-zA-Z0-9\._]+", formula)

    replaced_count = 0
    unreplaced_count = 0

    # ignored_names is set of strings that are not considered variables
    ignored_names = ["max", "min", "abs", "Distance"]
    for unit_type in types:
        ignored_names.append(str(unit_type.__name__))

    # for each matched formula:
    for var in vars:
        if var in ignored_names:
            continue

        # split variable name into separate keys
        keys = var.split('.')

        # try to match they key chain, starting from top of config dictionary
        value = top_cfg
        while len(keys) > 0:
            # if actual value is list, we assume key should be integer
            if isinstance(value, list):
                assert keys[0].isdigit()
                value = value[int(keys[0])]

            # if actual value is dictionary, we use key as string
            elif isinstance(value, dict):
                try:
                    value = value[keys[0]]
                except KeyError:
                    raise Exception(
                        "Using key for variable that does not exist ", keys[0],
                        " in variable: ", var
                    )
            else:
                raise Exception(
                    "Trying to access key ", keys[0],
                    " in part of config that is not list or dictionary: ", var
                )
            keys.pop(0)

        # 'value' should be actual value that shall be used in formula

        # If value is string and matches formula regex, it's unresolved.
        # We don't replace this value.
        if isinstance(value, str) and re.match("^\s*%(.*)%\s*$", value):
            unreplaced_count += 1
            continue

        # replace the value in formula
        formula = formula.replace(var, str(value))
        replaced_count += 1

    # if we found that not everything is replaced, we won't finish the formula
    if unreplaced_count > 0:
        formula = "%" + formula + "%"
        return ParseFormulaRes(
            replaced_count=replaced_count,
            unreplaced_count=unreplaced_count,
            val=formula
        )

    try:
        formula = eval(formula)
    except:
        # something goes wrong, don't compile
        formula = "%" + formula + "%"

    return ParseFormulaRes(
        replaced_count=replaced_count,
        unreplaced_count=unreplaced_count,
        val=formula
    )


def _parse_config_rec(cfg, top_cfg):
    """ Recursive parser of dictionary config, primary goal is to parse formulas.
        Should be only executed over list or dictionary. 
       
        Does not parse yaml files, it processes parsed yaml config file.

        Returns number of replaced values and unreplaced values"""

    # get set of keys, based on type of container
    keys = list()
    if isinstance(cfg, list):
        keys = range(len(cfg))
    elif isinstance(cfg, dict):
        keys = cfg.keys()

    replaced_big_count = 0
    unreplaced_big_count = 0

    for key in keys:
        # basic types are ignored
        if isinstance(cfg[key], (float, int, bool)):
            continue

        # in case of string, we try to parse it, it may be formula
        elif isinstance(cfg[key], str):
            res = _parse_val(
                cfg[key], top_cfg
            )
            replaced_big_count += res.replaced_count
            unreplaced_big_count += res.unreplaced_count
            cfg[key] = res.val

        # in case of known containers, recursion
        elif isinstance( cfg[key], (dict, list)):
            replaced_count, unreplaced_count = _parse_config_rec(
                cfg[key], top_cfg
            )
            replaced_big_count += replaced_count
            unreplaced_big_count += unreplaced_count
        
        else:
            raise Exception("Unknown type for configuration parsing")

    return replaced_big_count, unreplaced_big_count


def parse_config(filename):
    """ parses config from specified file.

        Returns as dict/list tree with all formulars resolved. """

    with open(filename, 'r') as content_file:
        content = content_file.read()
    config = yaml.load(content)

    # iterate till replaced_count is bigger than 0
    while _parse_config_rec(config, config)[0] > 0:
        pass

    # iterate again, if something was not replaced by this time, it's error
    if _parse_config_rec(config, config)[1] > 0:
        raise Exception(
            "Some values in config expressions were not replaced ", config
        )

    return config
