class ChromAbWidget(QWidget):
    def __init__(self, parent=None):
        super(ChromAbWidget, self).__init__(parent)

        self.maxD = 0.01
        self.deadZ = 5
        self.isShapeRadial = True
        self.isFalloffExp = True
        self.direction = 100
        self.interpolate = False
        self.numThreads = 4

        self.shapeInfo = QLabel("Shape and Direction:", self)
        self.shapeChoice = QButtonGroup(self)
        self.shapeBtn1 = QRadioButton("Radial")
        self.shapeBtn2 = QRadioButton("Linear")
        self.shapeChoice.addButton(self.shapeBtn1)
        self.shapeChoice.addButton(self.shapeBtn2)
        self.shapeBtn1.setChecked(True)
        self.shapeBtn1.pressed.connect(self.changeShape1)
        self.shapeBtn2.pressed.connect(self.changeShape2)

        self.theDial = QDial()
        self.theDial.setMinimum(0)
        self.theDial.setMaximum(359)
        self.theDial.setValue(100)
        self.theDial.setWrapping(True)
        self.theDial.setEnabled(False)
        self.theDial.valueChanged.connect(self.updateDial)

        self.maxInfo = QLabel("Max Displacement: 1%", self)
        self.maxDisplace = QSlider(Qt.Horizontal, self)
        self.maxDisplace.setRange(1, 500)
        self.maxDisplace.setValue(10)
        self.maxDisplace.valueChanged.connect(self.updateMax)

        self.falloffInfo = QLabel("Falloff:", self)
        self.falloffChoice = QButtonGroup(self)
        self.foBtn1 = QRadioButton("Exponential")
        self.foBtn2 = QRadioButton("Linear")
        self.falloffChoice.addButton(self.foBtn1)
        self.falloffChoice.addButton(self.foBtn2)
        self.foBtn1.setChecked(True)
        self.foBtn1.pressed.connect(self.changeFalloff1)
        self.foBtn2.pressed.connect(self.changeFalloff2)

        self.deadInfo = QLabel("Deadzone: 5%", self)
        self.deadzone = QSlider(Qt.Horizontal, self)
        self.deadzone.setRange(0, 100)
        self.deadzone.setValue(5)
        self.deadzone.valueChanged.connect(self.updateDead)

        self.biFilter = QCheckBox(
            "Bilinear Interpolation (slow, but smooths colors)", self)
        self.biFilter.stateChanged.connect(self.updateInterp)

        self.threadInfo = QLabel(
            "Number of Worker Threads (FOR ADVANCED USERS): 4", self)
        self.workThreads = QSlider(Qt.Horizontal, self)
        self.workThreads.setRange(1, 64)
        self.workThreads.setValue(4)
        self.workThreads.valueChanged.connect(self.updateThread)

        vbox = QVBoxLayout()
        vbox.addWidget(self.shapeInfo)
        vbox.addWidget(self.shapeBtn1)
        vbox.addWidget(self.shapeBtn2)
        vbox.addWidget(self.theDial)
        vbox.addWidget(self.maxInfo)
        vbox.addWidget(self.maxDisplace)
        vbox.addWidget(self.falloffInfo)
        vbox.addWidget(self.foBtn1)
        vbox.addWidget(self.foBtn2)
        vbox.addWidget(self.deadInfo)
        vbox.addWidget(self.deadzone)
        vbox.addWidget(self.biFilter)
        vbox.addWidget(self.threadInfo)
        vbox.addWidget(self.workThreads)

        self.setLayout(vbox)
        self.show()

    # Update labels and members
    def updateMax(self, value):
        self.maxInfo.setText("Max Displacement: " + str(value / 10) + "%")
        self.maxD = value / 1000

    def updateDead(self, value):
        self.deadInfo.setText("Deadzone: " + str(value) + "%")
        self.deadZ = value

    def changeShape1(self):
        self.isShapeRadial = True
        # Change UI so only valid options can be changed
        self.theDial.setEnabled(False)
        self.theDial.repaint()
        self.foBtn1.setEnabled(True)
        self.foBtn1.repaint()
        self.foBtn2.setEnabled(True)
        self.foBtn2.repaint()
        self.deadzone.setEnabled(True)
        self.deadzone.repaint()

    def changeShape2(self):
        self.isShapeRadial = False
        # Change UI so only valid options can be changed
        self.theDial.setEnabled(True)
        self.theDial.repaint()
        self.foBtn1.setEnabled(False)
        self.foBtn1.repaint()
        self.foBtn2.setEnabled(False)
        self.foBtn2.repaint()
        self.deadzone.setEnabled(False)
        self.deadzone.repaint()

    def changeFalloff1(self):
        self.isFalloffExp = True

    def changeFalloff2(self):
        self.isFalloffExp = False

    def updateDial(self, value):
        self.direction = value

    def updateInterp(self, state):
        if state == Qt.Checked:
            self.interpolate = True
        else:
            self.interpolate = False

    def updateThread(self, value):
        self.threadInfo.setText(
            "Number of Worker Threads (FOR ADVANCED USERS): " + str(value))
        self.numThreads = value

    # Required for main window to call into
    def getWindowName(self):
        return "Chromatic Aberration"

    def saveSettings(self, settings):
        settings.setValue("CA_maxD", self.maxD * 1000)
        settings.setValue("CA_deadZ", self.deadZ)
        if self.isShapeRadial:
            shape = 1
        else:
            shape = 0
        settings.setValue("CA_isShapeRadial", shape)
        if self.isFalloffExp:
            falloff = 1
        else:
            falloff = 0
        settings.setValue("CA_isFalloffExp", falloff)
        settings.setValue("CA_direction", self.direction)
        if self.interpolate:
            interp = 1
        else:
            interp = 0
        settings.setValue("CA_interpolate", interp)
        settings.setValue("CA_numThreads", self.numThreads)

    def readSettings(self, settings):
        self.updateMax(int(settings.value("CA_maxD", 10)))
        self.updateDead(int(settings.value("CA_deadZ", 5)))
        shapeRadial = int(settings.value("CA_isShapeRadial", 1))
        if shapeRadial == 1:
            self.isShapeRadial = True
        else:
            self.isShapeRadial = False
        falloffExp = int(settings.value("CA_isFalloffExp", 1))
        if falloffExp == 1:
            self.isFalloffExp = True
        else:
            self.isFalloffExp = False
        self.direction = int(settings.value("CA_direction", 100))
        interp = int(settings.value("CA_interpolate", 0))
        if interp == 1:
            self.interpolate = True
        else:
            self.interpolate = False
        self.updateThread(int(settings.value("CA_numThreads", 4)))
        # Update interactable UI elements
        self.theDial.setValue(self.direction)
        self.shapeBtn1.setChecked(self.isShapeRadial)
        self.shapeBtn2.setChecked(not self.isShapeRadial)
        self.maxDisplace.setValue(int(self.maxD * 1000))
        self.foBtn1.setChecked(self.isFalloffExp)
        self.foBtn2.setChecked(not self.isFalloffExp)
        self.deadzone.setValue(self.deadZ)
        self.biFilter.setChecked(self.interpolate)
        self.workThreads.setValue(self.numThreads)
        if self.isShapeRadial:
            self.changeShape1()
        else:
            self.changeShape2()

    def getBlendMode(self):
        return "normal"

    # Call into C library to process the image
    def applyFilter(self, imgData, imgSize):
        newData = create_string_buffer(imgSize[0] * imgSize[1] * 4)
        dll = GetSharedLibrary()
        imgCoords = Coords(imgSize[0], imgSize[1])
        # python makes it hard to get a pointer to existing buffers for some reason
        cimgData = c_char * len(imgData)
        threadPool = []
        interp = 0
        if self.interpolate:
            interp = 1
        if self.isShapeRadial:
            falloff = 0
            if self.isFalloffExp:
                falloff = 1
            filterSettings = RadialFilterData(int(self.maxD * imgSize[0]),
                                              self.deadZ, falloff, interp)
        else:
            filterSettings = LinearFilterData(int(self.maxD * imgSize[0]),
                                              self.direction, interp)
        idx = 0
        for i in range(self.numThreads):
            numPixels = (imgSize[0] * imgSize[1]) // self.numThreads
            if i == self.numThreads - 1:
                numPixels = (imgSize[0] * imgSize[1]
                             ) - idx  # Give the last thread the remainder
            if self.isShapeRadial:
                workerThread = Thread(target=dll.VFXRadialAberration,
                                      args=(
                                          idx,
                                          numPixels,
                                          filterSettings,
                                          imgCoords,
                                          cimgData.from_buffer(imgData),
                                          byref(newData),
                                      ))
            else:
                workerThread = Thread(target=dll.VFXLinearAberration,
                                      args=(
                                          idx,
                                          numPixels,
                                          filterSettings,
                                          imgCoords,
                                          cimgData.from_buffer(imgData),
                                          byref(newData),
                                      ))
            threadPool.append(workerThread)
            threadPool[i].start()
            idx += numPixels
        # Join threads to finish
        # If a crash happens, it would freeze here. User can still cancel though
        for i in range(self.numThreads):
            threadPool[i].join()
        return bytes(newData)

    def postFilter(self, app, doc, node):
        pass