import base64
import math
import random
import time

from PyQt5.QtCore import pyqtSlot, QSize, Qt
from PyQt5.QtGui import QFont
from PyQt5.QtGui import QPixmap, QImage, QMovie
from PyQt5.QtWidgets import QLabel, QSizePolicy

from piveilance.config import Config
from piveilance.util import ImageManip
from piveilance.util import parse_type


class Layout:

    def __init__(self,
                 id="unidentified",
                 type='flow',
                 cols=3,
                 rows=3,
                 maxAllowed=0,
                 cameras=None):
        self.id = parse_type(id, str)
        self.type = parse_type(type, str)
        self.cols = parse_type(cols, int)
        self.rows = parse_type(rows, int)
        self.maxAllowed = max(parse_type(maxAllowed, int), 0)
        self.cameras = {v['id']: v for v in parse_type(cameras, list)}
        self.geometry = WindowGeometry(self.rows, self.cols)

        for c in self.cameras.values():
            c['position'] = parse_type(c.get('position'), tuple)
            c['order'] = parse_type(c.get('order'), int)

    def calculate(self):
        pass

    def build(self, camMap):
        pass

    def updateGeometry(self, width, height, camLimit):
        self.maxAllowed = self.maxAllowed or camLimit
        self.geometry.width = width
        self.geometry.height = height
        self.calculate()


class FlowLayout(Layout):

    def __init__(self, *args, **kwargs):
        super(FlowLayout, self).__init__(*args, **kwargs)
        self.adjustNumberAllowed = True

    def calculate(self):

        """
        # Iterative computation to determine actual cols

        """
        # Start by initial guess that ncols = ncams
        g = self.geometry
        cols = rows = frameSize = self.maxAllowed
        while cols > 0:
            rows = math.ceil(self.maxAllowed / cols)
            frameSize = g.width / cols
            if ((g.height - frameSize * rows) < frameSize
                    and (rows * cols - self.maxAllowed) <= 1
                    or (rows > cols and cols > 2)):
                break
            cols -= 1
        cols = 1 if cols < 1 else cols

        g.rows = rows
        g.cols = cols
        g.frameSize = frameSize
        g.calculateAllProperties()

    def build(self, camMap):

        cams = camMap.values()

        for c in cams:
            if c.id in self.cameras:
                c.order = self.cameras[c.id].get('order')

        cams = sorted(cams, key=lambda x: x.order or len(cams) + 1)
        cams = cams[0: self.maxAllowed]

        for i, c in enumerate(cams):
            c.position = self.geometry.grid[i]
        return {c.id: c for c in cams}


class FixedLayout(Layout):
    adjustNumberAllowed = False

    def __init__(self, *args, **kwargs):
        super(FixedLayout, self).__init__(*args, **kwargs)
        self.adjustNumberAllowed = False

    def calculate(self):
        """
        # Rows and cols are predefined from the config

        """
        self.geometry.frameSize = self.geometry.width / self.geometry.cols
        self.geometry.calculateAllProperties()

    def build(self, camMap):

        positioned = {}
        freeCams = {c for c in camMap.values() if c.id not in self.cameras}
        camsByPosition = {c.get('position'): c for c in self.cameras.values()}

        for pos in self.geometry.grid:
            if pos in camsByPosition:
                camId = camsByPosition[pos]['id']
                cam = camMap.get(camId)
                if not cam:
                    cam = PlaceholderCamera(id=camId)
            else:
                if freeCams:
                    cam = freeCams.pop()
                    camId = cam.id
                else:
                    camId = "undefined-{}".format(pos)
                    cam = PlaceholderCamera(id=camId, name="Offline")
            cam.position = pos
            positioned[camId] = cam
        return positioned


class View():

    def __init__(self,
                 id,
                 fullscreen,
                 stretch,
                 fontRatio,
                 labels,
                 showCoords):
        self.id = parse_type(id, str)
        self.fullscreen = parse_type(fullscreen, bool)
        self.stretch = parse_type(stretch, bool)
        self.fontRatio = parse_type(fontRatio, float)
        self.labels = parse_type(labels, bool)
        self.showCoords = parse_type(showCoords, bool)

    def getCamGlobals(self):
        return {
            'stretch': self.stretch,
            'fontRatio': self.fontRatio,
            'showLabels': self.labels,
            'showCoords': self.showCoords,
        }


class Camera(QLabel):
    def __init__(self, **kwargs):
        super(Camera, self).__init__(None)
        self.setStaticOptions(Config(kwargs))

    def setStaticOptions(self, options):
        self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
        self.setMinimumSize(QSize(50, 50))
        self.label = QLabel()
        self.pixmap = None
        self.id = options['id']
        self.movie = None

        self.cropRatio = options.get_float('cropRatio', 0)
        if self.cropRatio < 0 or self.cropRatio > 1:
            raise ValueError("Crop cannot be negative or inverse (>1)")

        self.name = options.get('name', self.id)
        self.position = options.get('position', None)
        self.order = options.get('order', None)
        self.size = options.get_int('size', 50)
        self.showLabels = options.get_bool('showLabels', False)
        self.showCoords = options.get_bool('showCoords', True)
        self.fontRatio = options.get_float('fontRatio', 0.025)
        self.direction = options.get_string('direction', 'right')
        self.stretch = options.get_bool('stretch', False)
        self.setOptions({})

    @pyqtSlot(dict, name="updateCamOptions")
    def setOptions(self, options):
        # validate ?
        vars(self).update(options)
        self.setScaledContents(self.stretch)
        self.setFrameSize()
        self.setLabel()

    def setFrameSize(self):
        if self.pixmap:
            self.setPixmap(self.pixmap.scaled(self.size, self.size))

    def setLabel(self):
        # fit text if too big
        text = self.name if self.showLabels else ""
        if self.position and self.showCoords:
            text = str(self.position) + " " + text
        self.label.setText(text)
        self.label.setFont(QFont("Arial", self.fontRatio * self.size))
        self.label.setAlignment(Qt.AlignLeft | Qt.AlignTop)
        self.label.setStyleSheet("color: #05FF00; font-weight: bold")

    @pyqtSlot(object, name="setimage")
    def setImage(self, camData=None):
        pass


class PiCamera(Camera):
    def __init__(self, **kwargs):
        super(PiCamera, self).__init__(**kwargs)

    @pyqtSlot(object, name="setimage")
    def setImage(self, camData=None):
        if self.id in camData:
            data = self.getImage(camData[self.id]['image'])
            img = QImage()
            img.loadFromData(data)
            if self.cropRatio != 0:
                crop = max((img.width() - img.height()) * self.cropRatio, 0)
                img = ImageManip.crop_direction(img, crop, self.direction)

            time.sleep(0.005 * random.randint(0, 1))
            self.pixmap = QPixmap().fromImage(img)
            self.setFrameSize()

    def getImage(self, data):
        byte = data.encode('utf-8')
        return base64.decodebytes(byte)


class PlaceholderCamera(Camera):
    image = "resources/image/offline.gif"

    def __init__(self, **kwargs):
        super(PlaceholderCamera, self).__init__(**kwargs)
        self.setImage()

    def setFrameSize(self):
        if self.movie:
            self.movie.setScaledSize(QSize(self.size, self.size))

    def setImage(self, camData=None):
        self.movie = QMovie(self.image)
        self.setMovie(self.movie)
        self.setFrameSize()
        self.movie.start()


class WindowGeometry():

    def __init__(self, rows=0, cols=0):
        self.rows = rows
        self.cols = cols
        self.height = 0
        self.width = 0
        self.frameSize = 0
        self.margins = (0, 0, 0, 0)
        self.grid = []

    def total(self):
        return self.cols * self.rows

    def correctFrameSize(self):
        remainder_height = self.height - self.frameSize * self.rows
        if remainder_height < 0:
            self.frameSize = self.frameSize - abs(remainder_height / self.rows)

    def setMargins(self):
        rheight = max(self.height - self.frameSize * self.rows, 0) / 2
        vheight = max(self.width - self.frameSize * self.cols, 0) / 2
        self.margins = (vheight, rheight, vheight, rheight)

    def calculateAllProperties(self):
        self.grid = [(i, j) for i in range(self.rows) for j in range(self.cols)]
        self.correctFrameSize()
        self.setMargins()
