#!/usr/bin/env python3

import PySide6.QtCore as QtCore
import PySide6.QtGui as QtGui
import xasy2asy as xasy2asy

import PrimitiveShape
import math

import Widg_addPolyOpt
import Widg_addLabel


class InplaceObjProcess(QtCore.QObject):
    objectCreated = QtCore.Signal(QtCore.QObject)
    objectUpdated = QtCore.Signal()
    object_created_connection = None


    def __init__(self, parent=None):
        super().__init__(parent)
        self._active = False

    def connect_created_signal(self, handler):
        if self.object_created_connection is not None:
            self.objectCreated.disconnect(self.object_created_connection)
        self.object_created_connection = self.objectCreated.connect(handler)

    @property
    def active(self):
        return self._active

    def mouseDown(self, pos, info, mouseEvent: QtGui.QMouseEvent=None):
        raise NotImplementedError

    def mouseMove(self, pos, event: QtGui.QMouseEvent):
        raise NotImplementedError

    def mouseRelease(self):
        raise NotImplementedError

    def forceFinalize(self):
        raise NotImplementedError

    def getPreview(self):
        return None

    def getObject(self):
        raise NotImplementedError

    def getXasyObject(self):
        raise NotImplementedError

    def postDrawPreview(self, canvas: QtGui.QPainter):
        pass

    def createOptWidget(self, info):
        return None


class AddCircle(InplaceObjProcess):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.center = QtCore.QPointF(0, 0)
        self.radius = 0

    def mouseDown(self, pos, info, mouseEvent: QtGui.QMouseEvent=None):
        x, y = PrimitiveShape.PrimitiveShape.pos_to_tuple(pos)
        self.radius = 0
        self.center.setX(x)
        self.center.setY(y)
        self.fill = info['fill']
        self._active = True

    def mouseMove(self, pos, event):
        self.radius = PrimitiveShape.PrimitiveShape.euclideanNorm(pos, self.center)

    def mouseRelease(self):
        self.objectCreated.emit(self.getXasyObject())
        self._active = False

    def getPreview(self):
        x, y = PrimitiveShape.PrimitiveShape.pos_to_tuple(self.center)
        boundRect = QtCore.QRectF(x - self.radius, y - self.radius, 2 * self.radius, 2 * self.radius)
        # because the internal image is flipped...
        newPath = QtGui.QPainterPath()
        newPath.addEllipse(boundRect)
        # newPath.addRect(boundRect)
        return newPath

    def getObject(self):
        return PrimitiveShape.PrimitiveShape.circle(self.center, self.radius)

    def getXasyObject(self):
        if self.fill:
            newObj = xasy2asy.xasyFilledShape(self.getObject(), None)
        else:
            newObj = xasy2asy.xasyShape(self.getObject(), None)
        return newObj

    def forceFinalize(self):
        self.mouseRelease()


class AddLabel(InplaceObjProcess):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.alignMode = None
        self.opt = None
        self.text = None
        self.anchor = QtCore.QPointF(0, 0)
        self._active = False
        self.fontSize = 12

    def createOptWidget(self, info):
        self.opt = Widg_addLabel.Widg_addLabel(info)
        return self.opt

    def getPreview(self):
        return None

    def mouseRelease(self):
        self.objectCreated.emit(self.getXasyObject())
        self._active = False

    def mouseMove(self, pos, event):
        x, y = PrimitiveShape.PrimitiveShape.pos_to_tuple(pos)
        self.anchor.setX(x)
        self.anchor.setY(y)

    def mouseDown(self, pos, info, mouseEvent: QtGui.QMouseEvent=None):
        if self.opt is not None:
            self.text = self.opt.labelText
        x, y = PrimitiveShape.PrimitiveShape.pos_to_tuple(pos)
        self.anchor.setX(x)
        self.anchor.setY(y)

        self.alignMode = info['align']
        self.fontSize = info['fontSize']
        self._active = True

    def getObject(self):
        finalTuple = PrimitiveShape.PrimitiveShape.pos_to_tuple(self.anchor)
        return {'txt': self.text, 'align': str(self.alignMode), 'anchor': finalTuple}

    def getXasyObject(self):
        text = self.text
        align = str(self.alignMode)
        anchor = PrimitiveShape.PrimitiveShape.pos_to_tuple(self.anchor)
        newLabel = xasy2asy.xasyText(text=text, location=anchor, pen=None,
                                align=align, asyengine=None, fontsize=self.fontSize)
        newLabel.asyfied = False
        return newLabel

    def forceFinalize(self):
        self.mouseRelease()


class AddBezierShape(InplaceObjProcess):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.asyengine = None
        self.basePath = None
        self.basePathPreview = None
        self.closedPath = None
        self.info = None
        self.fill = False
        self.opt = None

        # list of "committed" points with Linkage information.
        # Linkmode should be to the last point.
        # (x, y, linkmode), (u, v, lm2) <==> (x, y) <=lm2=> (u, v)
        self.pointsList = []
        self.currentPoint = QtCore.QPointF(0, 0)
        self.pendingPoint = None
        self.useLegacy = False

    def mouseDown(self, pos, info, mouseEvent: QtGui.QMouseEvent=None):
        x, y = PrimitiveShape.PrimitiveShape.pos_to_tuple(pos)
        self.currentPoint.setX(x)
        self.currentPoint.setY(y)
        self.info = info

        if not self._active:
            self._active = True
            self.fill = info['fill']
            self.asyengine = info['asyengine']
            self.closedPath = info['closedPath']
            self.useBezierBase = info['useBezier']
            self.useLegacy = self.info['options']['useLegacyDrawMode']
            self.pointsList.clear()
            self.pointsList.append((x, y, None))
        else:
            # see http://doc.qt.io/archives/qt-4.8/qt.html#MouseButton-enum
            mouseClicked = mouseEvent is not None and mouseEvent.buttons() & QtCore.Qt.MouseButton.RightButton
            if mouseClicked and self.useLegacy:
                self.forceFinalize()

    def _getLinkType(self):
        if self.info['useBezier']:
            return '..'
        else:
            return '--'

    def mouseMove(self, pos, event):
        # in postscript coords.
        if self._active:
            x, y = PrimitiveShape.PrimitiveShape.pos_to_tuple(pos)

            if self.useLegacy or (event.buttons() & QtCore.Qt.MouseButton.AllButtons):
                self.currentPoint.setX(x)
                self.currentPoint.setY(y)
            else:
                self.forceFinalize()


    def createOptWidget(self, info):
        return None
        # self.opt = Widg_addBezierInPlace.Widg_addBezierInplace(info)
        # return self.opt

    def finalizeClosure(self):
        if self.active:
            self.closedPath = True
            self.forceFinalize()

    def mouseRelease(self):
        x, y = self.currentPoint.x(), self.currentPoint.y()
        self.pointsList.append((x, y, self._getLinkType()))
        # self.updateBasePath()

    def updateBasePath(self):
        self.basePath = xasy2asy.asyPath(asyengine=self.asyengine, forceCurve=self.useBezierBase)
        newNode = [(x, y) for x, y, _ in self.pointsList]
        newLink = [lnk for *args, lnk in self.pointsList[1:]]
        if self.useLegacy:
            newNode += [(self.currentPoint.x(), self.currentPoint.y())]
            newLink += [self._getLinkType()]
        if self.closedPath:
            newNode.append('cycle')
            newLink.append(self._getLinkType())
        self.basePath.initFromNodeList(newNode, newLink)

        if self.useBezierBase:
            self.basePath.computeControls()

    def updateBasePathPreview(self):
        self.basePathPreview = xasy2asy.asyPath(
            asyengine=self.asyengine, forceCurve=self.useBezierBase)
        newNode = [(x, y) for x, y, _ in self.pointsList] + [(self.currentPoint.x(), self.currentPoint.y())]
        newLink = [lnk for *args, lnk in self.pointsList[1:]] + [self._getLinkType()]
        if self.closedPath:
            newNode.append('cycle')
            newLink.append(self._getLinkType())
        self.basePathPreview.initFromNodeList(newNode, newLink)

        if self.useBezierBase:
            self.basePathPreview.computeControls()

    def forceFinalize(self):
        self.updateBasePath()
        self._active = False
        self.pointsList.clear()
        self.objectCreated.emit(self.getXasyObject())
        self.basePath = None

    def getObject(self):
        if self.basePath is None:
            raise RuntimeError('BasePath is None')
        self.basePath.asyengine = self.asyengine
        return self.basePath

    def getPreview(self):
        if self._active and self.pointsList:
            self.updateBasePathPreview()
            newPath = self.basePathPreview.toQPainterPath()
            return newPath
        return None

    def getXasyObject(self):
        if self.fill:
            return xasy2asy.xasyFilledShape(self.getObject(), None)
        else:
            return xasy2asy.xasyShape(self.getObject(), None)


class AddPoly(InplaceObjProcess):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.center = QtCore.QPointF(0, 0)
        self.currPos = QtCore.QPointF(0, 0)
        self.sides = None
        self.inscribed = None
        self.centermode = None
        self.asyengine = None
        self.fill = None
        self.opt = None

    def mouseDown(self, pos, info, mouseEvent: QtGui.QMouseEvent=None):
        self._active = True
        self.sides = info['sides']
        self.inscribed = info['inscribed']
        self.centermode = info['centermode']
        self.fill = info['fill']


        x, y = PrimitiveShape.PrimitiveShape.pos_to_tuple(pos)
        self.center.setX(x)
        self.center.setY(y)
        self.currPos = QtCore.QPointF(self.center)

    def mouseMove(self, pos, event):
        x, y = PrimitiveShape.PrimitiveShape.pos_to_tuple(pos)
        self.currPos.setX(x)
        self.currPos.setY(y)

    def mouseRelease(self):
        if self.active:
            self.objectCreated.emit(self.getXasyObject())
            self._active = False

    def forceFinalize(self):
        self.mouseRelease()

    def getObject(self):
        if self.inscribed:
            return PrimitiveShape.PrimitiveShape.inscribedRegPolygon(self.sides, self.center, self._rad(),
                                                                     self._angle())
        else:
            return PrimitiveShape.PrimitiveShape.exscribedRegPolygon(self.sides, self.center, self._rad(),
                                                                     self._angle())

    def getPreview(self):
        if self.inscribed:
            poly = PrimitiveShape.PrimitiveShape.inscribedRegPolygon(self.sides, self.center, self._rad(),
                                                                     self._angle(), qpoly=True)
        else:
            poly = PrimitiveShape.PrimitiveShape.exscribedRegPolygon(self.sides, self.center, self._rad(),
                                                                     self._angle(), qpoly=True)
        newPath = QtGui.QPainterPath()
        newPath.addPolygon(poly)
        return newPath

    def createOptWidget(self, info):
        self.opt = Widg_addPolyOpt.Widg_addPolyOpt(info)
        return self.opt

    def _rad(self):
        return PrimitiveShape.PrimitiveShape.euclideanNorm(self.currPos, self.center)

    def _angle(self):
        dist_x = self.currPos.x() - self.center.x()
        dist_y = self.currPos.y() - self.center.y()
        if dist_x == 0 and dist_y == 0:
            return 0
        else:
            return math.atan2(dist_y, dist_x)

    def getXasyObject(self):
        if self.fill:
            newObj = xasy2asy.xasyFilledShape(self.getObject(), None)
        else:
            newObj = xasy2asy.xasyShape(self.getObject(), None)
        return newObj

class AddFreehand(InplaceObjProcess):
    # TODO: At the moment this is just a copy-paste of the AddBezierObj.
    # Must find a better algorithm for constructing the obj rather than
    # a node for every pixel the mouse moves.
    def __init__(self, parent=None):
        super().__init__(parent)
        self.asyengine = None
        self.basePath = None
        self.basePathPreview = None
        self.closedPath = None
        self.info = None
        self.fill = False
        self.opt = None

        # list of "committed" points with Linkage information.
        # Linkmode should be to the last point.
        # (x, y, linkmode), (u, v, lm2) <==> (x, y) <=lm2=> (u, v)
        self.pointsList = []
        self.currentPoint = QtCore.QPointF(0, 0)
        self.pendingPoint = None
        self.useLegacy = False

    def mouseDown(self, pos, info, mouseEvent: QtGui.QMouseEvent=None):
        x, y = PrimitiveShape.PrimitiveShape.pos_to_tuple(pos)
        self.currentPoint.setX(x)
        self.currentPoint.setY(y)
        self.info = info

        if not self._active:
            self._active = True
            self.fill = info['fill']
            self.asyengine = info['asyengine']
            self.closedPath = info['closedPath']
            self.useBezierBase = info['useBezier']
            self.useLegacy = self.info['options']['useLegacyDrawMode']
            self.pointsList.clear()
            self.pointsList.append((x, y, None))
        else:
            # see http://doc.qt.io/archives/qt-4.8/qt.html#MouseButton-enum
            isRightClicked = mouseEvent is not None and mouseEvent.buttons() & QtGui.Qt.MouseButton.RightButton
            if isRightClicked and self.useLegacy:
                self.forceFinalize()

    def _getLinkType(self):
        if self.info['useBezier']:
            return '..'
        else:
            return '--'

    def mouseMove(self, pos, event):
        # in postscript coords.
        if self._active:
            x, y = PrimitiveShape.PrimitiveShape.pos_to_tuple(pos)

            if self.useLegacy or event.buttons() & QtCore.Qt.MouseButton.AllButtons:
                self.currentPoint.setX(x)
                self.currentPoint.setY(y)
                self.pointsList.append((x, y, self._getLinkType()))


    def createOptWidget(self, info):
        return None

    def mouseRelease(self):
        self.updateBasePath()
        self._active = False
        self.pointsList.clear()
        self.objectCreated.emit(self.getXasyObject())
        self.basePath = None

    def updateBasePath(self):
        self.basePath = xasy2asy.asyPath(asyengine=self.asyengine, forceCurve=self.useBezierBase)
        newNode = [(x, y) for x, y, _ in self.pointsList]
        newLink = [lnk for *args, lnk in self.pointsList[1:]]
        if self.useLegacy:
            newNode += [(self.currentPoint.x(), self.currentPoint.y())]
            newLink += [self._getLinkType()]
        if self.closedPath:
            newNode.append('cycle')
            newLink.append(self._getLinkType())
        self.basePath.initFromNodeList(newNode, newLink)

        if self.useBezierBase:
            self.basePath.computeControls()

    def updateBasePathPreview(self):
        self.basePathPreview = xasy2asy.asyPath(
            asyengine=self.asyengine, forceCurve=self.useBezierBase)
        newNode = [(x, y) for x, y, _ in self.pointsList] + [(self.currentPoint.x(), self.currentPoint.y())]
        newLink = [lnk for *args, lnk in self.pointsList[1:]] + [self._getLinkType()]
        if self.closedPath:
            newNode.append('cycle')
            newLink.append(self._getLinkType())
        self.basePathPreview.initFromNodeList(newNode, newLink)

        if self.useBezierBase:
            self.basePathPreview.computeControls()

    def getObject(self):
        if self.basePath is None:
            raise RuntimeError('BasePath is None')
        self.basePath.asyengine = self.asyengine
        return self.basePath

    def getPreview(self):
        if self._active and self.pointsList:
            self.updateBasePathPreview()
            return self.basePathPreview.toQPainterPath()
        return None

    def getXasyObject(self):
        self.fill = False
        return xasy2asy.xasyShape(self.getObject(), None)
