from typing import Union, Optional
from functools import total_ordering
from collections import defaultdict
import arrow
from itertools import tee

from .. import configuration
from ..dto.patch import PatchListDto

try:
    import ujson as json
except ImportError:
    import json

from ..data import Region, Season


def pairwise(iterable):
    "s -> (s0,s1), (s1,s2), (s2, s3), ..."
    a, b = tee(iterable)
    next(b, None)
    return zip(a, b)


@total_ordering
class Patch(object):
    __patches = None

    def __init__(self, region: Union[str, Region], season: Season, name: str, start: Union[arrow.Arrow, float], end: Optional[Union[arrow.Arrow, float]]):
        if not isinstance(start, arrow.Arrow):
            start = arrow.get(start)
        if end is not None and not isinstance(end, arrow.Arrow):
            end = arrow.get(end)
        if not isinstance(region, Region):
            region = Region(region)
        self._region = region
        self._season = season
        self._name = name
        self._start = start
        self._end = end

    def __str__(self) -> str:
        return self._name

    @classmethod
    def from_str(cls, string: str, region: Union[Region, str] = None) -> "Patch":
        if not cls.__patches:
            cls.__load__()
        if region is None:
            region = configuration.settings.default_region
        if not isinstance(region, Region):
            region = Region(region)
        for patch in cls.__patches[region]:
            if string in patch.name:
                return patch
        else:
            raise ValueError("Unknown patch name {}".format(string))

    @classmethod
    def from_date(cls, date: Union[arrow.Arrow], region: Union[Region, str] = None) -> "Patch":
        if not cls.__patches:
            cls.__load__()
        if region is None:
            region = configuration.settings.default_region
        if not isinstance(region, Region):
            region = Region(region)
        for patch in cls.__patches[region]:
            patch_end = patch.end or arrow.now().shift(seconds=1)
            if patch.start <= date < patch_end:
                return patch
        else:
            raise ValueError("Unknown patch date {}".format(date))

    @classmethod
    def latest(cls, region: Union[Region, str] = None) -> "Patch":
        if isinstance(region, str):
            region = Region(region)
        if cls.__patches is None:
            cls.__load__()
        return cls.__patches[region][-1]

    @classmethod
    def __load__(cls):
        data = configuration.settings.pipeline.get(PatchListDto, query={})
        patches = data["patches"]
        shifts = data["shifts"]
        cls.__patches = defaultdict(lambda: [None for _ in range(len(patches))])
        for i, (patch, next_patch) in enumerate(pairwise(patches)):
            for region in Region:
                start = arrow.get(patch["start"] + shifts[region.platform.value]).to(region.timezone)
                season = Season.from_id(patch["season"])
                name = patch["name"]
                end = arrow.get(next_patch["start"] + shifts[region.platform.value]).to(region.timezone)
                cls.__patches[region][i] = Patch(region=region, season=season, name=name, start=start, end=end)

        # Since pairwise skips the last patch in the list, add it manually here
        patch = patches[-1]
        for region in Region:
            start = arrow.get(patch["start"] + shifts[region.platform.value]).to(region.timezone)
            season = Season.from_id(patch["season"])
            name = patch["name"]
            end = None
            cls.__patches[region][-1] = Patch(region=region, season=season, name=name, start=start, end=end)

        # Sort each region's patches by start date
        for region in Region:
            cls.__patches[region].sort(key=lambda patch: patch.start)

    @property
    def region(self) -> Region:
        return self._region

    @property
    def season(self) -> Season:
        return self._season

    @property
    def name(self) -> str:
        return self._name

    @property
    def start(self) -> arrow.Arrow:
        return self._start

    @property
    def end(self) -> arrow.Arrow:
        return self._end

    @property
    def major(self) -> str:
        return self.name.split(".")[0]

    @property
    def minor(self) -> str:
        return self.name.split(".")[1]

    @property
    def majorminor(self) -> str:
        return ".".join(self.name.split(".")[:2])

    @property
    def revision(self) -> str:
        return ".".join(self.name.split(".")[2:])

    def __eq__(self, other: "Patch") -> bool:
        return self.name == other.name

    def __lt__(self, other: "Patch") -> bool:
        if int(self.major) < int(other.major) or (self.major == other.major and int(self.minor) < int(other.minor)):
            return True
        else:
            return False
