import datetime
import logging
import operator
from collections import deque

from django.db import models
from django.utils import timezone as tz
from django.utils.translation import gettext_lazy as _

from .diffusion import Diffusion
from .sound import Sound
from .station import Station
from .track import Track
from .page import Renderable

logger = logging.getLogger("aircox")


__all__ = ("Log", "LogQuerySet")


class LogQuerySet(models.QuerySet):
    def station(self, station=None, id=None):
        return self.filter(station=station) if id is None else self.filter(station_id=id)

    def date(self, date):
        start = tz.datetime.combine(date, datetime.time())
        end = tz.datetime.combine(date, datetime.time(23, 59, 59, 999))
        return self.filter(date__range=(start, end))
        # this filter does not work with mysql
        # return self.filter(date__date=date)

    def after(self, date):
        return self.filter(date__gte=date) if isinstance(date, tz.datetime) else self.filter(date__date__gte=date)

    def on_air(self):
        return self.filter(type=Log.TYPE_ON_AIR)

    def start(self):
        return self.filter(type=Log.TYPE_START)

    def with_diff(self, with_it=True):
        return self.filter(diffusion__isnull=not with_it)

    def with_sound(self, with_it=True):
        return self.filter(sound__isnull=not with_it)

    def with_track(self, with_it=True):
        return self.filter(track__isnull=not with_it)


class Log(Renderable, models.Model):
    """Log sounds and diffusions that are played on the station.

    This only remember what has been played on the outputs, not on each
    source; Source designate here which source is responsible of that.
    """

    template_prefix = "log"

    TYPE_STOP = 0x00
    """Source has been stopped, e.g. manually."""
    # Rule: \/ diffusion != null \/ sound != null
    TYPE_START = 0x01
    """Diffusion or sound has been request to be played."""
    TYPE_CANCEL = 0x02
    """Diffusion has been canceled."""
    # Rule: \/ sound != null /\ track == null
    #       \/ sound == null /\ track != null
    #       \/ sound == null /\ track == null /\ comment = sound_path
    TYPE_ON_AIR = 0x03
    """Sound or diffusion occured on air."""
    TYPE_OTHER = 0x04
    """Other log."""
    TYPE_CHOICES = (
        (TYPE_STOP, _("stop")),
        (TYPE_START, _("start")),
        (TYPE_CANCEL, _("cancelled")),
        (TYPE_ON_AIR, _("on air")),
        (TYPE_OTHER, _("other")),
    )

    station = models.ForeignKey(
        Station,
        models.CASCADE,
        verbose_name=_("station"),
        help_text=_("related station"),
    )
    type = models.SmallIntegerField(_("type"), choices=TYPE_CHOICES)
    date = models.DateTimeField(_("date"), default=tz.now, db_index=True)
    source = models.CharField(
        # we use a CharField to avoid loosing logs information if the
        # source is removed
        max_length=64,
        blank=True,
        null=True,
        verbose_name=_("source"),
        help_text=_("identifier of the source related to this log"),
    )
    comment = models.CharField(
        max_length=512,
        blank=True,
        null=True,
        verbose_name=_("comment"),
    )
    sound = models.ForeignKey(
        Sound,
        models.SET_NULL,
        blank=True,
        null=True,
        db_index=True,
        verbose_name=_("Sound"),
    )
    track = models.ForeignKey(
        Track,
        models.SET_NULL,
        blank=True,
        null=True,
        db_index=True,
        verbose_name=_("Track"),
    )
    diffusion = models.ForeignKey(
        Diffusion,
        models.SET_NULL,
        blank=True,
        null=True,
        db_index=True,
        verbose_name=_("Diffusion"),
    )

    objects = LogQuerySet.as_manager()

    @property
    def related(self):
        return self.diffusion or self.sound or self.track

    # FIXME: required????
    @property
    def local_date(self):
        """Return a version of self.date that is localized to self.timezone;
        This is needed since datetime are stored as UTC date and we want to get
        it as local time."""
        return tz.localtime(self.date, tz.get_current_timezone())

    # prepare for the future on crash + ease the use in merged lists with
    # diffusions
    @property
    def start(self):
        return self.date

    class Meta:
        verbose_name = _("Log")
        verbose_name_plural = _("Logs")

    def __str__(self):
        return "#{} ({}, {}, {})".format(
            self.pk,
            self.get_type_display(),
            self.source,
            self.local_date.strftime("%Y/%m/%d %H:%M%z"),
        )

    @classmethod
    def __list_append(cls, object_list, items):
        object_list += [cls(obj) for obj in items]

    @classmethod
    def merge_diffusions(cls, logs, diffs, count=None, diff_count=None, group_logs=False):
        """Merge logs and diffusions together.

        `logs` can either be a queryset or a list ordered by `Log.date`.
        """
        if isinstance(logs, models.QuerySet):
            logs = list(logs.order_by("-date"))
        diffs = diffs.on_air().order_by("-start")
        if diff_count:
            diffs = diffs[:diff_count]
        diffs = deque(diffs)
        object_list = []

        while True:
            if not len(diffs):
                cls._append_logs(object_list, logs, len(logs), group=group_logs)
                break

            if not len(logs):
                object_list += diffs
                break

            diff = diffs.popleft()

            # - takes all logs after diff start
            index = cls._next_index(logs, diff.end, len(logs), pred=operator.le)
            cls._append_logs(object_list, logs, index, group=group_logs)

            if len(logs):
                # FIXME
                # - last log while diff is running
                # if logs[0].date > diff.start:
                #    object_list.append(logs[0])

                # - skips logs while diff is running
                index = cls._next_index(logs, diff.start, len(logs))
                if index is not None and index > 0:
                    logs = logs[index:]

            # - add diff
            object_list.append(diff)

        return object_list if count is None else object_list[:count]

    @classmethod
    def _next_index(cls, items, date, default, pred=operator.lt):
        iter = (i for i, v in enumerate(items) if pred(v.date, date))
        return next(iter, default)

    @classmethod
    def _append_logs(cls, object_list, logs, count, group=False):
        logs = logs[:count]
        if not logs:
            return object_list

        if group:
            grouped = cls._group_logs_by_time(logs)
            object_list.extend(grouped)
        else:
            object_list += logs
        return object_list

    @classmethod
    def _group_logs_by_time(cls, logs):
        last_time = -1
        cum = []
        for log in logs:
            hour = log.date.time().hour
            if hour != last_time:
                if cum:
                    yield cum
                    cum = []
                last_time = hour
            # reverse from lowest to highest date
            cum.insert(0, log)
        if cum:
            yield cum

    def print(self):
        r = []
        if self.diffusion:
            r.append("diff: " + str(self.diffusion_id))
        if self.sound:
            r.append("sound: " + str(self.sound_id))
        if self.track:
            r.append("track: " + str(self.track_id))
        logger.info(
            "log %s: %s%s",
            str(self),
            self.comment or "",
            " (" + ", ".join(r) + ")" if r else "",
        )
