from PyQt5.QtWidgets import QMainWindow, QGraphicsScene, QGraphicsItem, QPushButton, \
    QGraphicsView, QGraphicsObject
from PyQt5.QtGui import QBrush, QColor, QPen, QTransform, QDrag, QIcon
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QRectF, QPoint, QPointF, QObject, \
    Qt, QMimeData

import math
import logging

from ui.ui_keys import Ui_Keys

from sql.models import Category, CategoryMember, CategoryGroup, CategoryFight
from settings import Settings
from ui.common import string_abbrev
from ui.member import MemberCreate, MemberSearch, GroupSearch
from ui.results import CategoryResults


logger = logging.getLogger('keys')


class KeysError(Exception):
    pass


SCENE_PAD = 20


class KeysGraphicsScene(QGraphicsScene):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)


class KeysView(QMainWindow, Ui_Keys):

    signalFight = pyqtSignal(object)

    def __init__(self, panel, *args, **kwargs):
        super().__init__(*args, **kwargs)
        Ui_Keys.setupUi(self, self)

        self.addDockWidget(Qt.LeftDockWidgetArea, self.dockWidget)
        scene = KeysGraphicsScene(self)
        self.GraphicsView.setScene(scene)
        self.GraphicsView.signalSelect.connect(self.__key_select)
        self.setWindowTitle('Kumite App - Llaves')
        self.setWindowIcon(QIcon(":images/logo.png"))

        self.CategoriesCombo.currentIndexChanged.connect(self._category_changed)
        self.MemberCreateButton.clicked.connect(self.__member_create)
        self.MemberSearchButton.clicked.connect(self.__member_search)
        self.KeyCreateButton.clicked.connect(self.__key_create)

        panel.signalStarted.connect(self._started_cb)
        panel.signalWinner.connect(self._winner_cb)
        Settings().colors_keys.signalUpdated.connect(self.__refresh)
        Settings().keys.signalUpdated.connect(self.__refresh)
        self.__refresh()
        self.results = CategoryResults(self)
        self.results.signalUpdate.connect(self.PositionGroupbox.update)
        self.__selected = None

    @pyqtSlot(object)
    def __key_select(self, key):
        editable = key.is_free4record() if key else False
        if editable and self.GroupCheckbox.isChecked():
            self.MemberCreateButton.setEnabled(False)
        else:
            self.MemberCreateButton.setEnabled(editable)
        self.MemberSearchButton.setEnabled(editable)
        if not editable:
            self.KeyCreateButton.setEnabled(False)

        index = key.index if key else ''
        record = key.record.item if (key and key.record) else None
        self.__showKey(index, record)

    def __showKey(self, index, record):
        self.KeyIndexEdit.setText(str(index))
        self.KeyNamesEdit.setText(record.label if record else '')
        self.KeyDojoEdit.setText(record.dojo.name if record else '')
        single = True if (record and not record.is_group) else False
        self.KeyBirthdayEdit.setText(str(record.birthday) if single else '')
        self.KeyGenderEdit.setText(record.gender_text if single else '')

    @pyqtSlot()
    def __refresh(self):
        auto = Settings().keys['automatic'].get()
        self.AutoButton.setChecked(auto)

        scene = self.GraphicsView.scene()
        scene.setBackgroundBrush(Settings().colors_keys['background'].get())
        scene.update()

    @pyqtSlot()
    def load_categories(self):
        """
        Block signals of CategoriesCombo to avoid that callback gets fired while
        the items are manipulated.
        """
        self.CategoriesCombo.blockSignals(True)
        self.CategoriesCombo.clear()
        for cat in Category.objects().all():
            self.CategoriesCombo.addItem(cat.label, userData=cat.id)

        logger.info("Reloading categories, total %i" % self.CategoriesCombo.count())
        cat_id = self.CategoriesCombo.currentData()
        cat = Category.objects().filter(Category.id==cat_id).first()
        self.CategoriesCombo.blockSignals(False)

        self.__load_category(cat)

    def _started_cb(self, started):
        self._running = started
        #self.StatusLabel.setText('Activo' if started else 'Libre')
        self.GraphicsView.setInteractive(not started)
        self.CategoriesCombo.setEnabled(not started)

    def _winner_cb(self, fight, number):
        if (type(fight) == str) or (fight is None):
            return

        winner = fight.set_winner(number).record
        loser = fight.loser.record
        logger.info("winner %i => %i" % (winner.index, fight.output.index))

        new = CategoryFight()
        new.index = fight.output.index
        new.category_id = fight.category_id
        new.winner_member_id = fight.winner_member_id
        new.loser_member_id = fight.loser_member_id
        new.winner_group_id = fight.winner_group_id
        new.loser_group_id = fight.loser_group_id
        new.third_place = fight.is_third
        new.save()
        # -- Unselect the selected key before refreshing the keys oetherwise will
        # -- automatically select the old one!
        self.__selected = None
        self.__fights.unselect()
        self._category_changed()

    def __load_category(self, category):
        logger.info('Loading new category %s' % category.label)
        self.CategoryAgesEdit.setText(f'{category.age_from} - {category.age_to}')
        self.CategoryGenderEdit.setText(category.gender_text)
        self.GroupCheckbox.setChecked(category.grouped)

        #  -- Remove all the previous items
        self.GraphicsView.scene().clear()

        key_h = 60
        key_w = 200

        # -- Load all the keys from the passed category
        records = category.groups if category.grouped else category.members
        keys = Keys(self.GraphicsView, category, records, width=key_w, height=key_h)

        for key in keys:
            self._key_add(keys, key)

        self.__fights = Fights(category, self.GraphicsView.scene(), keys)
        self.__fights.signalFight.connect(self._fight_selected)

        if self.__selected and self.__selected.fights.category.label == category.label:
            fight = self.__fights.get_from_index(self.__selected.index)
            self.__fights.select(fight)

        view_w = int(keys[-1].x()) + key_w
        view_h = int(keys.column(0)[-1].y()) + key_h
        self.GraphicsView.setSceneRect(0, 0, view_w + SCENE_PAD, view_h + SCENE_PAD)
        #self.GraphicsView.setFixedSize(view_w + 40, view_h + 40)
        # -- Schedule a painting of all the items
        self.__refresh()
        self.__keys = keys
        self.GraphicsView.reset()
        self.results.update(category)

    @pyqtSlot(object)
    def _fight_selected(self, fight):
        self.__selected = fight
        self.signalFight.emit(fight)

    def _key_add(self, keys, key):
        pos = self._key_position(keys, key)
        key.setPos(pos)
        self.GraphicsView.scene().addItem(key)

    def _key_position(self, keys, key):
        offset = QPoint(0, 0) if key.column else QPoint(SCENE_PAD, SCENE_PAD)
        pad_x = 80
        pad_y = 40
        x = (key.column * (key.width + pad_x))
        if key.column:
            key_prev = keys.key(key.column - 1, key.row * 2)
            key_next = keys.key(key.column - 1, key.row * 2 + 1)
            diff = key_next.y() - (key_prev.y() + key_prev.height)
            y = key_next.y() - diff / 2 - key_next.height / 2
        else:
            y = (key.row * (key.height + pad_y))

        return QPointF(x, y) + offset

    def _category_changed(self, *args):
        cat_id = self.CategoriesCombo.currentData()
        cat = Category.objects().filter(Category.id==cat_id).first()
        self.__load_category(cat)

    @pyqtSlot()
    def close(self):
        self.hide()
        self.CategoriesCombo.blockSignals(True)
        self.CategoriesCombo.clear()
        self.CategoriesCombo.blockSignals(False)
        self.__keys = None

    @property
    def has_categories(self):
        return self.CategoriesCombo.count()

    def load_fight(self):
        if not self.__keys:
            raise KeysError('No hay llaves cargadas.')

        for nr in range(0, self.__keys.columns - 1):
            for fight in self.__fights.column(nr):
                if fight.is_pending:
                    logger.info('Found pending fight in column %i' % nr)
                    fight.load()
                    return

        # -- Check if there is a pending fight for third position
        if self.results.pending_third:
            first = self.__cast_to_key(self.results.pending_third[0])
            second = self.__cast_to_key(self.results.pending_third[1])
            output = Key(self.__keys, -1, 0, 0, None)
            logger.info("Creating third place fight, %s and %s" % (first, second))
            # -- Set scene to None for the third place and create a dummy output
            fight = Fight(self.__fights, None, first, second, output, -1)
            fight.load()
            return

        raise KeysError(
            'No hay pelea pendiente en categoría %s' % self.__keys.category.label
        )

    def __cast_to_key(self, record):
        if type(record) == Key:
            return record
        elif type(record) == CategoryMember:
            return self.__keys.keyFromCategoryMember(record)
        elif type(record) == CategoryFight:
            return self.__keys.keyFromCategoryFight(record)
        else:
            raise TypeError('Unknown record type %s to cast' % type(record))

    @pyqtSlot()
    def __member_create(self):
        dlg = MemberCreate(category=self.__keys.category)
        if not dlg.exec_():
            return
        if dlg.record:
            self.__new_key_prepare(dlg.record)

    @pyqtSlot()
    def __member_search(self):
        if self.__keys.category.grouped:
            dlg = GroupSearch(category=self.__keys.category)
        else:
            dlg = MemberSearch(category=self.__keys.category)
        if not dlg.exec_():
            return
        if dlg.record:
            self.__new_key_prepare(dlg.record)

    def __new_key_prepare(self, record):
        self.__new_member = record
        self.__showKey(self.KeyIndexEdit.text(), record)
        self.KeyCreateButton.setEnabled(True)

    @pyqtSlot()
    def __key_create(self):
        # -- Add this new created member to the selected category
        entry = CategoryMember(category_id=self.__keys.category.category_id,
                               index=int(self.KeyIndexEdit.text()),
                               member_id=self.__new_member.member_id)
        logger.warning('Saving new key')
        entry.save()
        self.__new_member = None
        self._category_changed()


class Keys(list):
    def __init__(self, view, category, records, **kwargs):
        super().__init__()
        self.view = view
        self.category = category
        self.count = len(records)
        self.columns = math.ceil(self.count / 8) + 3
        logger.info('Load category %s with %i records and %i columns' % (category.label,
                                                                         self.count,
                                                                         self.columns))
        self.rows_first = self._calc_first(self.count)
        self.indexes = list(range(0, self.rows_first * 2 - 1))

        fights = []
        for f in category.fights:
            if not f.third_place:
                fights.append(f)

        self._build_keys(records + fights, self.indexes, **kwargs)
        self.logger = logging.getLogger('Keys')
        self.logger.info("Created %i keys with indexes %s" % (len(self),
                                                              repr(self.indexes)))

    @property
    def is_done(self):
        i = self.key(self.columns - 1, 0)
        return i.has_record

    @property
    def is_grouped(self):
        return self.category.grouped

    @staticmethod
    def _calc_first(count):
        base = int(count / 8)
        if count % 8:
            base += base or 1
        return base * 8

    def column_rows(self, column):
        """ Return rows number of a column"""
        if column >= self.columns:
            raise IndexError(f'Column number {column} exceeds maximum {self.columns}')
        return (self.rows_first >> column)

    def column(self, number, only_records=False):
        """ Return all keys that belong to a passed column number """
        if number < 0:
            number = self.columns + number

        rows = self.column_rows(number)
        first = self.key(number, 0)
        items = [self.__getitem__(x) for x in range(first.index, first.index + rows)]
        if only_records:
            return [x for x in items if x.has_record]
        return items

    def position(self, index):
        """ Returns a tupple with the two indexes: column and row """
        column = 0
        rows = 0
        last_rows = 0
        while column < self.columns:
            rows += self.column_rows(column)
            if rows > index:
                return (column, index - last_rows)
            column += 1
            last_rows = rows
        raise IndexError(f'Index {index} doesnt exist.')

    def key(self, column, row):
        if column >= self.columns:
            raise IndexError(f'Column number {column} exceeds maximum {self.columns}')
        elif row >= self.column_rows(column):
            raise IndexError(f'Row number {row} exceeds maximum {self.column_rows(column)} for column {column}')

        count = 0
        while column > 0:
            count += (self.rows_first >> (column - 1))
            column -= 1

        return self[count + row]

    def _build_keys(self, records, indexes, **kwargs):
        record_indexes = dict([(r.index, r) for r in records])
        for index in indexes:
            column, row = self.position(index)
            record = record_indexes.get(index)
            self._add_key(self, index, column, row, record, **kwargs)

        for key in self:
            key.update_drag()

    def _add_key(self, *args, **kwargs):
        key = Key(*args, **kwargs)
        self.append(key)

    def keyByIndex(self, index: int):
        for key in self:
            if key.index == index:
                return key

    def keyFromCategoryMember(self, record: CategoryMember):
        for key in self:
            if key.record_id == record.id:
                return key

    def keyFromCategoryFight(self, record: CategoryFight):
        for key in self:
            if key.record_id == record.id:
                return key


class Key(QGraphicsObject):

    PEN_WIDTH = 4

    def __init__(self, keys,
                 index: int,
                 column: int,
                 row: int,
                 record, #: CategoryMember|None,
                 *args, height=40, width=200,
                 **kwargs) -> None:
        self.keys = keys
        self.index = index
        self.column = column
        self.row = row
        self.record = record
        self.height = height
        self.width = width
        super().__init__(*args, **kwargs)
        self.rect = QRectF(0, 0, self.width, self.height)
        self._high = False
        # -- Always enable drops but in the EnterEvent it's decided to accept or not
        self.setAcceptDrops(True)
        # -- Dont update at this point as some keys are not ready
        #self.__update()

    def update_drag(self):
        self.setCursor(Qt.OpenHandCursor if self.is_movable else Qt.ArrowCursor)
        self.setAcceptedMouseButtons(Qt.LeftButton)
        self.update()

    def __repr__(self):
        return "Key %i %i@%i" % (self.index, self.column, self.row)

    def boundingRect(self):
        return self.rect

    def paint(self, painter, option, widget):
        color = Settings().colors_keys['frame_aka' if self.index % 2 else 'frame_ao'].get()
        pen = QPen(color)
        pen.setWidth(5 if self._high else 3)
        painter.setPen(pen)
        color.setAlpha(140 if self._high else 60)
        painter.setBrush(color)
        painter.drawRoundedRect(self.rect, 10, 10)

        if self.record:
            sz = Settings().keys['font_size'].get()
            font = painter.font()
            font.setFamily("BankGothic Md BT")
            font.setPixelSize((sz + 2) if self._high else sz)
            font.setBold(self._high)
            painter.setFont(font)

            painter.setPen(QPen(Settings().colors_keys['font'].get()))
            x = self.rect.topLeft().x() + 10
            y = self.rect.center().y()
            painter.drawText(QPointF(x, y - 5),
                             string_abbrev(self.record.item.label, 20))
            painter.drawText(QPointF(x, y + 15),
                             string_abbrev(self.record.item.dojo.name, 20))

    @property
    def output(self):
        # -- pos() returns the position of the item in parent coordinates
        p = self.pos()
        return QPointF(p.x() + self.width, p.y() + self.height/2)

    @property
    def input(self):
        # -- pos() returns the position of the item in parent coordinates
        p = self.pos()
        return QPointF(p.x(), p.y() + self.height/2)

    @property
    def has_record(self):
        return self.record is not None

    @property
    def record_id(self):
        return self.record and self.record.id or None

    def dump(self):
        print(f'Key index {self.index}')
        if self.has_record:
            print(f'  Record ID : {self.record.id}')
            print(f'  Item label: {self.record.item.label}')
            print(f'  Dojo      : {self.record.item.dojo.name}')

    def is_free4record(self, **kwargs):
        if not self.has_record and not self.is_locked(**kwargs):
            return True
        else:
            return False

    @property
    def is_movable(self):
        if self.has_record and not self.is_locked():
            return True
        else:
            return False

    def is_locked(self, ignore=None):
        # -- First check the previous keys
        count = 2
        row = self.row
        for col in range(self.column - 1, -1, -1):
            for i in range(0, count):
                key = self.keys.key(col, 2 * row + i)
                if key.has_record and key.index != ignore:
                    return True

            count *= 2
            row *= 2

        # -- Check next keys
        row = self.row
        for col in range(self.column + 1, self.keys.columns):
            key = self.keys.key(col, int(row/2))
            #print(col, "=>", repr(key))
            if key.has_record and key.index != ignore:
                return True
            row = int(row/2)

        return False

    def select(self):
        self._high = True
        self.update()

    def unselect(self):
        self._high = False
        self.update()

    def dragEnterEvent(self, ev):
        if ev.mimeData().hasText():
            index = int(ev.mimeData().text())
            #print("%s: dragEnterEvent %i" % (repr(self), index))
            ev.setAccepted(self.is_free4record(ignore=index))

    def dropEvent(self, ev):
        # ev: QGraphicsSceneDragDropEvent
        index = int(ev.mimeData().text())
        key = self.keys.keyByIndex(index)
        print("%s: dropEvent %i" % (repr(self), key.index))
        self.record = key.record
        key.record = None
        self.update_drag()
        key.update_drag()
        self.record.update(index=self.index)

    def mousePressEvent(self, ev):
        if self.is_movable:
            drag = QDrag(self)
            mimeData = QMimeData()
            mimeData.setText(str(self.index))
            drag.setMimeData(mimeData)
            drag.exec_()


class Fights(QObject):

    signalFight = pyqtSignal(object)

    def __init__(self, category, scene, keys):
        super().__init__()
        self.category = category
        self.scene = scene
        self._build(scene, keys)

    @property
    def is_grouped(self):
        return self.category.grouped

    def _build(self, scene, keys):
        fights = []
        columns = {}
        column = 1
        while column < keys.columns:
            inputs = keys.column(column - 1)
            inputs = [(inputs[i], inputs[i+1]) for i in range(0, len(inputs), 2)]
            columns[column - 1] = []
            for ins, out in list(zip(inputs, keys.column(column))):
                fight = Fight(self, scene, ins[0], ins[1], out, len(fights) + 1)
                fights.append(fight)
                columns[column - 1].append(fight)

            column += 1

        self._fights = fights
        self._columns = columns

    def get_from_input(self, index):
        for fight in self._fights:
            if fight.has_input(index):
                return fight
        raise IndexError(f'No winner for index {index}')

    def get_from_index(self, index):
        for fight in self._fights:
            if fight.index == index:
                return fight
        raise IndexError(f'No fight index {index}')

    @property
    def is_grouped(self):
        return self.category.grouped

    def column(self, nr):
        return self._columns[nr]

    def unselect(self):
        for fight in self._fights:
            fight.unselect()

    def select(self, fight):
        self.unselect()
        fight.select()
        self.signalFight.emit(fight)
        self.scene.update()


class Fight(QGraphicsItem):

    BRUSH_COLOR = QColor(55, 55, 55)

    def __init__(self,
                 fights: Fights,
                 scene, #: KeysGraphicsScene|None,
                 first: Key,
                 second: Key,
                 output,
                 index: int, *args, **kwargs) -> None:
        """
        A fight connects three keys: two inputs and one output. Only some inputs
        might have a record assigned to it. See Key class.
        """
        self.fights = fights
        self.first = first
        self.second = second
        self.output = output
        self.index = index
        self.winner = None
        self.loser = None
        self.__selected = False
        self.is_third = index == -1
        super().__init__(*args, **kwargs)
        # If no scene passed, skipt all paiting operations
        if scene is None:
            return

        origin = first.output
        end = QPointF(output.input.x(), second.output.y())
        self.rect = QRectF(origin, end)
        scene.addItem(self)

        button = QPushButton(str(index))
        button.setFixedSize(20, 20)
        button.setFocusPolicy(Qt.NoFocus)
        scene.addWidget(button)
        button.move(self.rect.center().toPoint() -
                    QPoint(int(button.width() / 2), int(button.height() / 2)))
        button.released.connect(self.load)
        self.button = button

    @property
    def is_pending(self):
        """
        Check if both inputs have a record and the output is not completed, so the
        fight can be loaded to the panel
        """
        return (not self.output.has_record and (self.first.has_record and
                                                self.second.has_record))

    def boundingRect(self):
        return self.rect

    def paint(self, painter, option, widget):
        pen = QPen(Settings().colors_keys['lines'].get())
        pen.setWidth(6 if self.__selected else 2)
        painter.setPen(pen)

        if (not Settings().keys['automatic'].get() and
            (self.first.has_record and self.second.has_record and
             not self.output.has_record)):
            self.button.setEnabled(True)
        else:
            self.button.setEnabled(False)

        p0 = self.first.output
        p1 = p0 + QPointF(self.rect.width()/2, 0)
        p2 = self.rect.center()
        p3 = self.second.output
        p4 = p3 + QPointF(self.rect.width()/2, 0)
        painter.drawLine(p0, p1)
        painter.drawLine(p1, p2)
        painter.drawLine(p2, p4)
        painter.drawLine(p3, p4)
        painter.drawLine(p2, self.output.input)

    def load(self):
        # -- DONT add any code to this method as selection implemented in Fights() too
        self.fights.select(self)

    def has_input(self, index):
        return True if index in (self.first.index, self.second.index) else False

    def set_winner(self, number):
        if self.output.has_record:
            raise RuntimeError(
                f'set_winner() output key already has record {self.output.record.id}')

        self.winner = self.first if number == 1 else self.second
        self.loser = self.first if number == 2 else self.second
        return self.winner

    @property
    def category_id(self):
        return self.first.record.category_id

    def dump(self):
        print('Fight')
        print('First', self.first)
        print('Second', self.second)
        print('Output', self.output)
        print('Winner', self.winner)
        print('Grouped:', 'Yes' if self.fights.is_grouped else 'No')

    @property
    def winner_member_id(self):
        return self.winner.record.item_id if not self.fights.is_grouped else 0

    @property
    def loser_member_id(self):
        return self.loser.record.item_id if not self.fights.is_grouped else 0

    @property
    def winner_group_id(self):
        return self.winner.record.item_id if self.fights.is_grouped else 0

    @property
    def loser_group_id(self):
        return self.loser.record.item_id if self.fights.is_grouped else 0

    @property
    def is_writable(self):
        return False

    def unselect(self):
        self.__selected = False

    def select(self):
        self.__selected = True
