from functools import cmp_to_key

from datetime import datetime, timedelta

from staff.gap.controllers.utils import datetime_from_utc
from staff.lib.utils.date import parse_datetime


class LinkedGaps:
    def __init__(self, gaps):
        self.head = None
        self.tail = None
        self.len = 0

        for gap in gaps:
            link = LinkedGaps.Link(
                gap['date_from'],
                gap['date_to'],
                {
                    'full_day': gap['full_day'],
                    'work_in_absence': gap['work_in_absence'],
                    'workflow': gap['workflow'],
                })

            if not self.head:
                self.head = link

            if self.tail:
                self.add_link_after(link, self.tail)
            else:
                self.tail = link

    def add_link_after(self, link, prev_link):
        link.prev_link = prev_link
        if prev_link.next_link:
            link.next_link = prev_link.next_link
            prev_link.next_link = link
        else:
            self.tail = link
        prev_link.next_link = link
        self.len += 1

    def remove_link(self, link):
        prev_link = link.prev_link
        next_link = link.next_link

        if prev_link:
            prev_link.next_link = next_link
        else:
            self.head = next_link

        if next_link:
            next_link.prev_link = prev_link
        else:
            self.tail = prev_link

        self.len -= 1

    def exhaust(self):
        while self.head:
            max_gap_index, result = self._next_range()
            right_date = result['date_to']
            yield result
            self._purge_till(right_date, max_gap_index)

    def _purge_till(self, right_date, max_gap_index):
        gap_index = 0
        link = self.head
        while link and gap_index < max_gap_index:
            gap_index += 1
            if link.date_from < right_date:
                link.date_from = right_date
            if link.is_wrong_dates():
                next_link = link.next_link
                self.remove_link(link)
                link = next_link
            else:
                link = link.next_link

    def _next_range(self):
        if not self.head:
            return

        link = self.head
        left_date, right_date = link.date_from, link.date_to
        max_gap_index = 0
        data = []
        while link:
            max_gap_index += 1
            date_from = link.date_from
            date_to = link.date_to
            if date_from != left_date:
                if date_from < right_date:
                    right_date = date_from
                elif date_to < right_date:
                    right_date = date_to
                break
            elif date_to < right_date:
                right_date = date_to

            data.append(link.data)
            link = link.next_link

        return max_gap_index, {
            'date_from': left_date,
            'date_to': right_date,
            'data': data,
        }

    def __iter__(self):
        return LinkedGaps.Iter(self)

    class Link(object):

        def __init__(self, date_from, date_to, data):
            self.next_link = None
            self.prev_link = None
            self.date_from = date_from
            self.date_to = date_to
            self.data = data

        def as_dict(self):
            return {
                'date_from': self.date_from,
                'date_to': self.date_to,
                'data': self.data,
            }

        def is_wrong_dates(self):
            return self.date_from >= self.date_to

    class Iter(object):

        def __init__(self, linked_gaps):
            self.link = linked_gaps.head

        def __iter__(self):
            return self

        def __next__(self):
            if self.link:
                result = self.link
                self.link = self.link.next_link
                return result
            raise StopIteration()


class Availability(object):

    def __init__(self, date_from, date_to, timezone,
                 working_hour_from=None, working_hour_to=None, ignore_work_in_absence=False):
        self.ignore_work_in_absence = ignore_work_in_absence
        self.root = None
        self.gaps = []

        self.timezone = timezone

        self.date_from = datetime_from_utc(date_from, self.timezone)
        self.date_to = datetime_from_utc(date_to, self.timezone)

        if working_hour_from is not None and working_hour_to is not None:
            self._apply_working_hours(working_hour_from, working_hour_to)
        else:
            self.root = Availability.Link(self.date_from, self.date_to)

    def add_gap(self, gap):
        if gap['date_from'] < gap['date_to']:
            if not gap['full_day']:
                gap['date_from'] = datetime_from_utc(gap['date_from'], self.timezone)
                gap['date_to'] = datetime_from_utc(gap['date_to'], self.timezone)
            self.gaps.append(gap)

    def apply_gaps(self):
        self.gaps.sort(key=lambda x: x['date_from'])

        for gaps_range in LinkedGaps(self.gaps).exhaust():
            if not self.is_range_available(gaps_range['data']):
                self.eat(gaps_range['date_from'], gaps_range['date_to'])

    def is_range_available(self, range_data):
        if not range_data:
            return False

        if self.ignore_work_in_absence:
            return False

        range_data.sort(key=cmp_to_key(_cmp_ranges))

        return range_data[0]['work_in_absence']

    def _apply_working_hours(self, hour_from, hour_to):
        ranges = []
        day_from = self.date_from
        while True:
            day_from = day_from.replace(hour=hour_from, minute=0, second=0)
            day_to = day_from.replace(hour=hour_to) + timedelta(days=(1 if hour_to < hour_from else 0))

            if day_from > self.date_to:
                break

            if day_from < self.date_from:
                day_from = self.date_from
            if day_to > self.date_to:
                day_to = self.date_to

            ranges.append({'date_from': day_from, 'date_to': day_to})

            day_from = day_from + timedelta(days=1)

        prev_link = None
        for r in ranges:
            link = Availability.Link(r['date_from'], r['date_to'])
            if prev_link:
                prev_link.next_link = link
                link.prev_link = prev_link
            else:
                self.root = link
            prev_link = link

    def apply_holidays(self, holidays):
        for holiday in holidays or []:
            date_from = datetime.combine(holiday['date'], datetime.min.time())
            date_to = date_from + timedelta(days=1)
            self.eat(date_from, date_to)

    def apply_calendar_events(self, events):
        for event in events:
            if event['availability'] == 'busy':
                date_from = datetime_from_utc(parse_datetime(event['start']), self.timezone)
                date_to = datetime_from_utc(parse_datetime(event['end']), self.timezone)
                self.eat(date_from, date_to)

    def eat(self, date_from, date_to):
        if not self.root:
            return

        link = self.root
        while link:
            if date_to <= link.date_from:
                return
            elif date_from > link.date_from and date_to < link.date_to:  # разбивает
                self._divide_link(link, date_from, date_to)
                return
            elif date_from >= link.date_to:
                link = link.next_link
                continue

            if date_from <= link.date_from and date_to >= link.date_to:  # полностью покрывает
                next_link = link.next_link
                self._delete_link(link)
                link = next_link
                continue
            elif date_from > link.date_from and date_to >= link.date_to:  # откусывает справа
                link.date_to = date_from
            elif date_to < link.date_to and date_from <= link.date_from:  # откусывает слева
                link.date_from = date_to

            link = link.next_link

    def _delete_link(self, link):
        if link.prev_link:
            link.prev_link.next_link = link.next_link
        else:
            self.root = link.next_link
        if link.next_link:
            link.next_link.prev_link = link.prev_link

    def _divide_link(self, link, date_from, date_to):
        left_link = Availability.Link(link.date_from, date_from)
        right_link = Availability.Link(date_to, link.date_to)

        left_link.next_link = right_link
        right_link.prev_link = left_link

        if link.prev_link:
            link.prev_link.next_link = left_link
            left_link.prev_link = link.prev_link
        else:
            self.root = left_link
        if link.next_link:
            link.next_link.prev_link = right_link
            right_link.next_link = link.next_link

    def is_available(self, date):
        link = self.root
        while link:
            if date < link.date_from:
                return False
            if link.date_from <= date <= link.date_to:
                return True
            link = link.next_link
        return False

    def available_seconds(self):
        result = 0
        link = self.root
        while link:
            result += int((link.date_to - link.date_from).total_seconds())
            link = link.next_link
        return result

    def total_seconds(self):
        return int((self.date_to - self.date_from).total_seconds())

    def __iter__(self):
        link = self.root
        while link:
            yield link
            link = link.next_link

    class Link(object):

        def __init__(self, date_from, date_to):
            self.next_link = None
            self.prev_link = None
            self.date_from = date_from
            self.date_to = date_to

        def as_dict(self):
            return {
                'date_from': self.date_from,
                'date_to': self.date_to,
            }


def _cmp_ranges(a, b):
    if a['full_day'] and not b['full_day']:
        return 1
    elif not a['full_day'] and b['full_day']:
        return -1

    if a['work_in_absence'] and not b['work_in_absence']:
        return -1
    elif not a['work_in_absence'] and b['work_in_absence']:
        return 1

    return 0
