from decimal import Decimal
from typing import Union, Optional, Mapping, Set

from ee import EeException

__all__ = [
    "Money",
    "MoneyContext",
    "get_default_context",
    "set_default_context",
]


def _parse(amount: str) -> (str, str):
    def read_space():
        nonlocal idx
        while idx < end:
            if amount[idx] != " ":
                break

            idx += 1

    def read_num():
        nonlocal idx
        tmp = ""
        while idx < end:
            c_ = amount[idx]
            if not c_.isnumeric():
                break
            tmp += c_
            idx += 1

        return tmp

    def read_word():
        nonlocal idx
        tmp = ""
        while idx < end:
            c_ = amount[idx]

            if c_.isnumeric() or c_ in (" ", ",", "."):
                break

            tmp += c_
            idx += 1

        return tmp

    amount = amount.strip()

    idx = 0
    end = len(amount)

    if end == 0:
        return None, None

    # extract leading currency
    prefix = read_word()

    read_space()

    # If the number starts with '.' or ',' we assume it is a whole separator
    # if idx < end:
    #     c = amount[idx]
    #     if c == "." or c == ",":
    #         s1 = c
    #         idx += 1

    num = read_num()
    # if len(num) > 3:
    #     return None, None

    # First and second separators
    s1 = s2 = None
    s0s = []  # stuff before first separator
    s1s = []
    s2s = []

    if num:
        s0s.append(num)

    while idx < end:
        c = amount[idx]

        is_separator = c in (",", ".")

        if not is_separator and c == " ":
            next_c = amount[idx + 1] if idx + 1 < end else None

            if next_c is None or not next_c.isnumeric():
                idx += 1
                break

            is_separator = True

        if not is_separator:
            return None, None

        if not s1:
            s1 = c
        elif s1 and c == s1:
            pass
        elif s1 and c != s1 and not s2:
            s2 = c
        else:
            return None, None
        idx += 1

        num = read_num()
        if not num:
            if idx == end:
                break
            return None, None

        if s2:
            s2s.append(num)
        else:
            s1s.append(num)

        del num

    if s1 == ' ':
        s1 = s2
        s2 = None
        s0s.extend(s1s)
        s1s = s2s
        s2s = []

    if not s1 and not s2:
        wholes = "".join(s0s)
        parts = "0"
    elif s1 and not s2:
        wholes = "".join(s0s)
        parts = "".join(s1s)
    elif s1 and s2:
        wholes = "".join(s0s) + "".join(s1s)
        parts = "".join(s2s)
    else:
        return None, None

    read_space()

    postfix = read_word()

    # Check that there is no junk left
    if amount[idx:].strip():
        return None, None

    # wholes = wholes if wholes else "0"
    # parts = parts if parts else "0"

    currency = None
    if len(prefix):
        currency = prefix

    if len(postfix):
        if currency:
            return None, None
        currency = postfix

    while parts.endswith("0"):
        parts = parts[:-1]

    return wholes + "." + parts, currency


class Money(object):
    def __init__(self, amount: Decimal, currency: Optional[str] = None):
        self.amount: Decimal = amount
        self.currency = currency

    def assert_same_currency(self, other):
        if isinstance(other, int):
            return

        if self.currency is None and other.currency is None:
            return
        if self.currency == other.currency:
            return

        raise EeException("Can't relate to instances of Money with different currencies.")

    def __truediv__(self, other: Union["Money", int]):
        self.assert_same_currency(other)

        if isinstance(other, int):
            return Money(amount=self.amount.__truediv__(other), currency=self.currency)

        return Money(amount=self.amount.__truediv__(other), currency=self.currency)

    def __mul__(self, other: Union["Money", int]):
        self.assert_same_currency(other)

        if isinstance(other, int):
            return Money(amount=self.amount.__mul__(other), currency=self.currency)

        return Money(amount=self.amount.__mul__(other), currency=self.currency)

    def _amount_str(self):
        amount = str(self.amount)

        (sign, _int, _exp) = self.amount.as_tuple()
        if _exp >= 0:
            return amount

        while amount.endswith("0"):
            amount = amount[0:-1]

        if amount.endswith("."):
            amount = amount[0:-1]

        return amount

    def __repr__(self):
        return self._amount_str() + ((" " + self.currency) if self.currency else "")

    def __str__(self):
        amount = self._amount_str()

        if self.currency is None:
            return amount

        first, last = (self.currency, amount) if self.currency in _prefix_currencies else (amount, self.currency)

        return first + " " + last

    def __eq__(self, other: "Money"):
        if not isinstance(other, Money):
            return False

        return self.amount == other.amount and self.currency == other.currency


class MoneyContext(object):
    def __init__(self, symbols_to_currency: Mapping[str, str], prefix_currencies: Set[str]):
        self.symbols_to_currency = symbols_to_currency
        self.prefix_currencies = prefix_currencies

    @staticmethod
    def parse(amount: Union[str, int, float, Decimal], currency: Optional[str] = None) -> Money:
        m = MoneyContext.try_parse(amount, currency)

        if m is None:
            raise EeException("Could not parse money: '{}'".format(amount))

        return m

    @staticmethod
    def try_parse(amount: Union[str, int, float, Decimal], currency: Optional[str] = None) -> Optional[Money]:
        if isinstance(amount, str):
            (a, c) = _parse(amount)

            if a is None:
                return None

            amount = Decimal(a)

            if c is not None:
                if currency is not None and currency != c:
                    return None

                currency = c
        elif isinstance(amount, int) or isinstance(amount, float):
            amount = Decimal(amount)
        elif isinstance(amount, Decimal):
            pass
        else:
            raise EeException("Unsupported value type: {}".format(type(amount)))

        if currency is not None:
            currency = _symbols_to_currency.get(currency, currency)

        return Money(amount, currency)


_symbols_to_currency = {
    "$": "USD"
}

_prefix_currencies = {
    "USD",
}

_default_context = None


def get_default_context():
    global _default_context
    if _default_context is None:
        _default_context = MoneyContext(_symbols_to_currency, _prefix_currencies)

    return _default_context


def set_default_context(ctx: MoneyContext):
    global _default_context
    _default_context = ctx