import math
import numpy as np
import re
from functools import total_ordering
from ee.formatting import eng_str

__all__ = [
    "EeException",
    "EeVal",
    "read_ltspice_raw"
]


class EeException(Exception):
    pass


@total_ordering
class EeVal(object):
    from decimal import Decimal
    units = ['F',
             'Ohm', '\u2126',  # Ohm symbol
             'H']
    exponents = {
        'f': -15,
        'p': -12,
        'n': -9,
        'u': -6,
        '\u00B5': -6,  # The micro symbol
        'm': -3,

        'k': 3,
        'M': 6,
        'G': 9,
        'T': 12,
        'P': 15,
        'E': 18,
    }
    r = re.compile(
        "([0-9]+\\.[0-9]+|\\.?[0-9]+|[0-9]+\\.?) *([" + "".join(exponents.keys()) + "]?) *(" + "|".join(units) + "?)")

    def __init__(self, s=None, value=None, exp=None, unit=None):
        # This is where I regret having a string in the API at once.
        if s:
            val = EeVal.parse(s)
            (self._value, self._exp, self._unit) = (val._value, val._exp, val._unit)
        else:
            if value is None or exp is None:
                raise EeException("Bad arguments, value and exp has to be set.")
            (self._value, self._exp, self._unit) = (value, exp, unit)

    @staticmethod
    def parse(s) -> "EeVal":
        m = EeVal.r.match(s)
        if not m:
            raise EeException("Could not parse value: " + str(s))
        gs = m.groups()
        value = float(gs[0])
        exp = gs[1].strip()
        exp = EeVal.exponents.get(exp) if len(exp) > 0 else 0
        unit = gs[2]
        if value != 0 and value < 1:
            e = math.ceil(math.log10(value))
            exp = exp + e
            value = value * math.pow(10, -e)
        return EeVal(None, value=float(value), exp=exp, unit=unit if len(unit) > 0 else None)

    @property
    def value(self):
        return self.__float__()

    @property
    def unit(self):
        return self._unit

    def set(self, value=None, exp=None, unit=None):
        return EeVal(None,
                     value=value if value else self._value,
                     exp=exp if exp else self._exp,
                     unit=unit if unit else self._unit)

    def __hash__(self):
        return hash((self.__float__(), self._unit))

    def __eq__(self, other):
        return math.isclose(self.__float__(), other.__float__()) and self._unit == other._unit

    def __lt__(self, other):
        # return ((self.__float__(), self._unit) < (other.__float__(), other._unit))

        x = self.__float__() < other.__float__()
        if x != 0:
            return x
        if self._unit is None and other._unit is None:
            return 0
        if self._unit is not None and other._unit is not None:
            return self._unit < other._unit

        return 1 if self.unit is not None else -1

    def __str__(self):
        return eng_str(self.__float__(), self._unit)

    def __float__(self):
        return self._value * math.pow(10, self._exp)


class LtSpiceRaw(object):
    def __init__(self, variables, values_first, values_rest):
        self.variables = variables
        self.values_first = values_first
        self.values_rest = values_rest

    def get_values(self, variable):
        return self.values[variable.idx]

    def get_variable(self, idx=None, expression=None):
        if idx is not None:
            return self.variables[idx]
        if expression is not None:
            v = [v for v in self.variables if v.expression == expression]
            if len(v) != 1:
                raise Exception('Unknown variable: ' + str(v))
            return v[0]
        raise Exception('idx or expression must be given')

    def to_pandas(self):
        import pandas
        data = []
        for (i, v) in enumerate(self.values_first):
            data.append(pandas.Series(v, dtype='float64'))

        data = {}
        for (i, v) in enumerate(self.values_first):
            data[self.variables[i].expression] = v

        return pandas.DataFrame(data=data)


class LtSpiceVariable(object):
    def __init__(self, idx, expression, kind):
        self.idx = idx
        self.expression = expression
        self.kind = kind


class LtSpiceReader(object):
    def __init__(self, f):
        self.f = f
        self.mode = 'header'
        self.headers = {}
        self.variables = []
        self.values = []

    def read_header(self, line):
        sep = line.find(':')
        key = line[0:sep]
        value = line[sep + 1:].strip()

        #    print("key='{}', value='{}'".format(key, str(value)))

        if key == 'Binary':
            self.set_binary_mode()
        elif key == 'Variables':
            self.no_variables = int(self.headers['No. Variables'])
            self.mode = 'variables'
        else:
            self.headers[key] = value

    def read_variable(self, line):
        parts = line.split('\t')
        idx = int(parts[1])
        expression = parts[2]
        kind = parts[3]
        self.variables.append(LtSpiceVariable(idx, expression, kind))

        if len(self.variables) == self.no_variables:
            self.mode = 'header'

    def set_binary_mode(self):
        self.mode = 'binary'

    def read_binary(self):
        pos = self.f.tell()
        no_points = int(self.headers['No. Points'])
        no_variables = int(self.headers['No. Variables'])

        #    print("no_points={}, no_variables={}".format(no_points, no_variables))

        if True:
            self.values = np.zeros((no_variables, no_points), dtype='float32')

            for p in range(no_points):
                self.values[0][p] = np.fromfile(self.f, count=1, dtype='float64')
                row = np.fromfile(self.f, count=no_variables - 1, dtype='float32');
                for v in range(0, len(row)):
                    self.values[v + 1][p] = row[v]
        else:
            self.values = np.zeros((no_points, no_variables), dtype='float64')

            for p in range(no_points):
                self.values[p][0] = np.fromfile(self.f, count=1, dtype='float64')
                row = np.fromfile(self.f, count=no_variables - 1, dtype='float32');
                for v in range(0, len(row)):
                    self.values[p][v + 1] = row[v]

    def read(self):
        while True:
            line = self.f.readline();

            if len(line) == 0:
                break

            self.f.read(1)  # The data is utf16 encoded so read the extra null byte

            # if this wasn't the last line .readline() includes the newline
            if line[-1] == '\n' or line[-1] == 0x0a:
                line = line[:-1]

            # print("len(line)={}, line={}".format(len(line), str(line)))
            line = line.decode(encoding="utf-16")

            if self.mode == 'header':
                self.read_header(line)
            elif self.mode == 'variables':
                self.read_variable(line)

            # The binary data can't be read with readline()
            if self.mode == 'binary':
                #        self.f.read()
                self.read_binary()
                break

        if len(self.variables) == 0:
            raise Exception("Something didn't quite work when parsing file, no variables found")

        return LtSpiceRaw(self.variables, self.values, [])


def read_ltspice_raw(filename):
    with open(filename, mode="rb") as f:
        r = LtSpiceReader(f)
        return r.read()