# uHDR: HDR image editing software
#   Copyright (C) 2021  remi cozot 
#
#    This program is free software: you can redistribute it and/or modify
#    it under the terms of the GNU General Public License as published by
#    the Free Software Foundation, either version 3 of the License, or
#    (at your option) any later version.
#
#    This program is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU General Public License for more details.
#
# hdrCore project 2020
# author: remi.cozot@univ-littoral.fr

# -----------------------------------------------------------------------------
# --- VERSION TRACKING --------------------------------------------------------
# -----------------------------------------------------------------------------

# -----------------------------------------------------------------------------
# (1): DEBUG
#      HDRImageViewer doesn't open 
#      add time.sleep() in HDRviewerController.displayFile(...)
#      ---- Rémi Cozot, June 2021
# -----------------------------------------------------------------------------
# (2): Prevent Freezing
#      Add multithreading in:
#      i  - processpipe processing (except for export and display)
#      ii - loading images
#      ---- Rémi Cozot, July 2021
# -----------------------------------------------------------------------------
# (3): Prevent Freezing and speed up
#      Add multithreading in:
#      i  - processpipe processing for HDR display
#      ii - processpipe procssing export single image to vesaHDR1000
#      ---- Rémi Cozot, July 2021
# -----------------------------------------------------------------------------
# (4): Fix ev value and accuracy in color editor 
#      ---- Rémi Cozot, July 2021
# -----------------------------------------------------------------------------
# (5): Fix Y curve plot 
#      ---- Rémi Cozot, July 2021
# -----------------------------------------------------------------------------
# (6): Ability to export SDR (.jpg) image to HDR (tone expansion).
#      ---- Rémi Cozot, July 2021
# -----------------------------------------------------------------------------


# -----------------------------------------------------------------------------
# --- Package hdrGUI ---------------------------------------------------------
# -----------------------------------------------------------------------------
"""
package hdrGUI consists of the classes for GUI:
(1) controller classes:
    class GalleryMode(enum.Enum)
    class ImageWidgetController
    class ImageGalleryController()
    class AppController(object)
    class MultiDockController()
    class EditImageController
    class ImageInfoController
    class AdvanceSliderController()
    class ToneCurveController()
    class LightnessMaskController()
    class HDRviewerController()
    class LchColorSelectorController()
    class GeometryController()
(2) model classes: 
    class ImageWidgetModel(object)
    class ImageGalleryModel
    class AppModel(object)
    class EditImageModel(object)
    class AdvanceSliderModel()
    class ToneCurveModel()
    class LightnessMaskModel()
    class ImageInfoModel(object)
    class HDRviewerModel(object)
    class ImageQualityModel(object)
    class LchColorSelectorModel(object)
    class GeometryModel(object)
(3) view classes:
    class ImageWidgetView(QWidget)
    class FigureWidget(FigureCanvas)
    class ImageGalleryView(QSplitter)
    class AppView(QMainWindow)
    class ImageInfoView(QSplitter)
    class AdvanceLineEdit(object)
    class AdvanceCheckBox(object)
    class EditImageView(QSplitter)
    class MultiDockView(QDockWidget)
    class AdvanceSliderView(QFrame)
    class ToneCurveView(QFrame)
    class LightnessMaskView(QGroupBox)
    class HDRviewerView(QFrame)
    class ImageQualityView(QScrollArea)
    class LchColorSelectorView(QFrame)
    class GeometryView(QFrame)
(4) Qt multi threading classes:
    class RequestCompute(object)
    class RunCompute(QRunnable)
    class RequestLoadImage(object)
    class RunLoadImage(QRunnable)
    class pCompute(object)
    class pRun(QRunnable)
"""
# -----------------------------------------------------------------------------
# --- Import ------------------------------------------------------------------
# -----------------------------------------------------------------------------

import enum, sys, subprocess, copy, colour, time, os, shutil, datetime
import numpy as np
# pyQT5 import
from PyQt5.QtWidgets import QFileDialog, QApplication
from PyQt5.QtWidgets import QMessageBox

from . import model, view, thread
import hdrCore.image, hdrCore.processing, hdrCore.utils
import preferences.preferences as pref

# -----------------------------------------------------------------------------
# --- package methods ---------------------------------------------------------
# -----------------------------------------------------------------------------
def getScreenSize(app):
    screens = app.screens()
    res = list(map(lambda x: x.size(), screens))
    return res
# -----------------------------------------------------------------------------
# --- Class GalleryMode -------------------------------------------------------
# -----------------------------------------------------------------------------
class GalleryMode(enum.Enum):
    """ Enum  """

    _1x1         = 0         # 
    _3x2         = 1         # 
    _6x4         = 2         # 
    _9x6         = 3         #
    
    _2x1        =  4

    def nbRow(m):
        if m == GalleryMode._1x1: return 1
        if m == GalleryMode._3x2: return 2
        if m == GalleryMode._6x4: return 4
        if m == GalleryMode._9x6: return 6

        if m == GalleryMode._2x1: return 1
    def nbCol(m):
        if m == GalleryMode._1x1: return 1
        if m == GalleryMode._3x2: return 3
        if m == GalleryMode._6x4: return 6
        if m == GalleryMode._9x6: return 9

        if m == GalleryMode._2x1: return 2
# -----------------------------------------------------------------------------
# --- Class ImageWidgetController ---------------------------------------------
# -----------------------------------------------------------------------------
class ImageWidgetController:
    """ image widget controller """

    def __init__(self, image=None,id = -1):

        self.model = model.ImageWidgetModel(self)
        self.view = view.ImageWidgetView(self)

        self._id = id # store an (unique) id 

        if isinstance(image, (np.ndarray, hdrCore.image.Image)):
            self.model.setImage(image)
            self.view.setPixmap(self.model.getColorData())

    def setImage(self, image):
        self.model.setImage(image)
        return self.view.setPixmap(self.model.getColorData())

    def setQPixmap(self, qPixmap):
        self.view.setQPixmap(qPixmap)

    def id(self): return self._id
# -----------------------------------------------------------------------------
# --- Class ImageGalleryController --------------------------------------------
# -----------------------------------------------------------------------------
class ImageGalleryController():
    """ image gallery controller """

    def __init__(self, parent):
        if pref.verbose: print(" [CONTROL] >> ImageGalleryController.__init__()")

        self.parent = parent    # AppView
        self.view = view.ImageGalleryView(self)
        self.model = model.ImageGalleryModel(self)

    def setImages(self, imageFiles): self.model.setImages(imageFiles)

    def updateImages(self):
        """ called by ImageGalleryModel to ask for update images """
        self.view.pageNumber = 0 # reset page number
        self.view.updateImages()

    def callBackButton_previousPage(self):  
        self.view.changePageNumber(-1)
        if self.view.shapeMode == GalleryMode._1x1 : self.selectImage(0)

    def callBackButton_nextPage(self):      
        self.view.changePageNumber(+1)
        if self.view.shapeMode == GalleryMode._1x1 : self.selectImage(0)

    def computePageNumberOnGalleryModeChange(self,newGalleryMode):
        currentPage = self.view.pageNumber
        nbImagePerPage = GalleryMode.nbRow(self.view.shapeMode)*GalleryMode.nbCol(self.view.shapeMode)
        selectedImage = self.model.selectedImage() if (self.model.selectedImage()!=-1) else currentPage*nbImagePerPage
        newNbImagePerPage = GalleryMode.nbRow(newGalleryMode)*GalleryMode.nbCol(newGalleryMode)

        newPageNumber = selectedImage//newNbImagePerPage

        return newPageNumber

    def callBackButton_1x1(self):
        if self.view.shapeMode != GalleryMode._1x1:
            self.view.resetGridLayoutWidgets()
            self.view.pageNumber = self.computePageNumberOnGalleryModeChange(GalleryMode._1x1)
            self.view.shapeMode = GalleryMode._1x1
            self.view.buildGridLayoutWidgets()
            self.view.updateImages()
            self.model.loadPage(self.view.pageNumber)
            self.view.repaint()

    def callBackButton_3x2(self): 
        if self.view.shapeMode != GalleryMode._3x2:
            self.view.resetGridLayoutWidgets()
            self.view.pageNumber = self.computePageNumberOnGalleryModeChange(GalleryMode._3x2)
            self.view.shapeMode = GalleryMode._3x2
            self.view.buildGridLayoutWidgets()
            self.view.updateImages()
            self.model.loadPage(self.view.pageNumber)
            self.view.repaint()

    def callBackButton_6x4(self): 
        if self.view.shapeMode != GalleryMode._6x4:
            self.view.resetGridLayoutWidgets()
            self.view.pageNumber = self.computePageNumberOnGalleryModeChange(GalleryMode._6x4)
            self.view.shapeMode = GalleryMode._6x4
            self.view.buildGridLayoutWidgets()
            self.view.updateImages()
            self.model.loadPage(self.view.pageNumber)
            self.view.repaint()

    def callBackButton_9x6(self):
        if self.view.shapeMode != GalleryMode._9x6:
            self.view.resetGridLayoutWidgets()
            self.view.pageNumber = self.computePageNumberOnGalleryModeChange(GalleryMode._9x6)
            self.view.shapeMode = GalleryMode._9x6
            self.view.buildGridLayoutWidgets()
            self.view.updateImages()
            self.model.loadPage(self.view.pageNumber)
            self.view.repaint()

    def callBackButton_2x1(self):
        if self.view.shapeMode != GalleryMode._2x1:
            self.view.resetGridLayoutWidgets()
            self.view.pageNumber = self.computePageNumberOnGalleryModeChange(GalleryMode._2x1)
            self.view.shapeMode = GalleryMode._2x1
            self.view.buildGridLayoutWidgets()
            self.view.updateImages()
            self.model.loadPage(self.view.pageNumber)
            self.view.repaint()

    def selectImage(self, id):
        if pref.verbose: print(" [CONTROL] >> ImageGalleryController.selectImage()")

        nbImagePage = GalleryMode.nbRow(self.view.shapeMode)*GalleryMode.nbCol(self.view.shapeMode)
        idxImage = self.view.pageNumber*nbImagePage+id
        # check id
        if (idxImage < len(self.model.processPipes)):
            # update selected image
            processPipe = self.model.processPipes[idxImage]
            if processPipe:
                if self.parent.dock.setProcessPipe(processPipe):
                    self.model.setSelectedImage(idxImage)

    def getSelectedProcessPipe(self):
        if pref.verbose:  print(" [CONTROL] >> ImageGalleryController.getSelectedProcessPipe()")
        return self.model.getSelectedProcessPipe()

    def setProcessPipeWidgetQPixmap(self, qPixmap):
        idxProcessPipe = self.model.selectedImage()
        nbImagePage = GalleryMode.nbRow(self.view.shapeMode)*GalleryMode.nbCol(self.view.shapeMode)
        idxImageWidget = idxProcessPipe%nbImagePage
        if pref.verbose:  print(" >> ImageGalleryController.setProcessPipeWidgetQPixmap(...)[ image id:",idxProcessPipe,">> image widget controller:",idxImageWidget,"]")
        self.view.imagesControllers[idxImageWidget].setQPixmap(qPixmap)

    def save(self):
        if pref.verbose: print(" [CONTROL] >> ImageGalleryController.save()")
        self.model.save()

    def currentPage(self): return self.view.currentPage()

    def pageIdx(self):
        nb = self.currentPage()
        nbImagePage = GalleryMode.nbRow(self.view.shapeMode)*GalleryMode.nbCol(self.view.shapeMode)
        return (nb*nbImagePage), ((nb+1)*nbImagePage)

    def getFilenamesOfCurrentPage(self): return self.model.getFilenamesOfCurrentPage()
    def getProcessPipeById(self,i) : return self.model.getProcessPipeById(i)
    def getProcessPipes(self): return self.model.processPipes
# -----------------------------------------------------------------------------
# --- Class AppController -----------------------------------------------------
# -----------------------------------------------------------------------------
class AppController(object):
    """controller for MainWindow
    
        Attributes:
            screenSize 
            hdrDisplay 
            view                          
            model
            
        Methods:
            callBackInfo
            callBackSelectDir
            callBackSave
            callBackQuit
            callBackDisplayHDR
            callBackEndDisplay
            callBackCloseDisplayHDR
            callBackCompareRawEditedHDR
            callBackExportHDR
            callBackEndExportHDR
            callBackExportAllHDR

    """

    def __init__(self, app):
        if pref.verbose: print(" [CONTROL] >> AppController.__init__()")

        self.screenSize = getScreenSize(app)# get screens size

        # attributes
        self.hdrDisplay = HDRviewerController(self)
        self.view =  view.AppView(self, HDRcontroller = self.hdrDisplay)                         
        self.model = model.AppModel(self)
        
        self.view.show()

    def callBackPython(self):
        if pref.verbose:  print(" [CONTROL] >> AppController.callBackPython()")
        pref.computation = pref.target[0]

    def callBackNumba(self):
        if pref.verbose:  print(" [CONTROL] >> AppController.callBackNumba()")
        pref.computation = pref.target[1]

    def callBackCuda(self):
        if pref.verbose:  print(" [CONTROL] >> AppController.callBackCuda()")
        pref.computation = pref.target[2]

    def callBackSelectDir(self):
        if pref.verbose: print(" [CONTROL] >> AppController.callBackSelectDir()")
        if not self.view.assessmentRunning(): 
            dirName = QFileDialog.getExistingDirectory(None, 'Select Directory', self.model.directory)
            if dirName != "":
                # save current images (metadata)
                self.view.imageGalleryController.save()
                # get images in the selected directory
                self.view.imageGalleryController.setImages(self.model.setDirectory(dirName))
                self.hdrDisplay.displaySplash()

        else: self.view.statusBar().showMessage('stop assessment before') 

    def callBackSave(self): self.view.imageGalleryController.save()

    def callBackQuit(self):
        if pref.verbose: print(" [CB] >> AppController.callBackQuit()")
        if not self.view.assessmentRunning(): 
            self.view.imageGalleryController.save()
            self.hdrDisplay.close()
            sys.exit()
        else:
            okCancel = okCancelBox('uHDR: WARNING', 'closing now, assessment results will be lost!')
            if okCancel==QMessageBox.Ok:
                self.view.imageGalleryController.save()
                self.hdrDisplay.close()
                sys.exit()

    def callBackDisplayHDR(self):
        if pref.verbose:  print(" [CONTROL] >> AppController.callBackDisplayHDR()")

        selectedProcessPipe = self.view.imageGalleryController.model.getSelectedProcessPipe()

        if selectedProcessPipe:
            self.view.statusBar().showMessage('displaying HDR image, full size image computation: start, please wait !')
            self.view.statusBar().repaint()
            # save current processpipe metada
            originalImage = copy.deepcopy(selectedProcessPipe.originalImage)
            originalImage.metadata.metadata['processpipe'] = selectedProcessPipe.toDict()
            originalImage.metadata.save()

            # load full size image
            img = hdrCore.image.Image.read(originalImage.path+'/'+originalImage.name)

            # turn off: autoResize
            hdrCore.processing.ProcessPipe.autoResize = False 
            # make a copy of selectedProcessPipe  
            processpipe = copy.deepcopy(selectedProcessPipe)

            # set size to display size
            size = self.hdrDisplay.model.displayModel['shape']
            img = img.process(hdrCore.processing.resize(),size=(None, size[1]))

            # set image to process-pipe
            processpipe.setImage(img)

            # star parallel computation
            nbWidth,  nbHeight = 3,3
            thread.pCompute(self.callBackEndDisplay, processpipe,nbWidth,nbHeight, toneMap=False, progress=self.view.statusBar().showMessage)

    def callBackEndDisplay(self, img, addParams=None):
        if pref.verbose:  print(" [CONTROL] >> AppController.callBackEndDisplay()")

        # turn off: autoResize
        hdrCore.processing.ProcessPipe.autoResize = True  
        
        self.view.statusBar().showMessage('displaying HDR image, full size image computation: done !')

        # clip, scale
        img = img.process(hdrCore.processing.clip())
        img.colorData = img.colorData*hdrCore.utils.HDRdisplay['vesaDisplayHDR1000']['scaling']

        colour.write_image(img.colorData,"temp.hdr", method='Imageio') # local copy for display
        self.hdrDisplay.displayFile("temp.hdr")

    def callBackCloseDisplayHDR(self):
        if pref.verbose: print(" [CONTROL] >> AppController.callBackCloseDisplayHDR()")
        if not self.view.assessmentRunning(): self.hdrDisplay.displaySplash()
        else: self.view.statusBar().showMessage('stop assessment before') 

    def callBackCompareRawEditedHDR(self):
        """
        Callback of compare raw/edited HDR menu

        Display side by side original image and edited version.        
        """

        if pref.verbose:  print(" [CONTROL] >> AppController.callBackCompareOriginalInputHDR()")

        if not self.view.assessmentRunning():
            # process real size image
            # get selected process pipe
            selectedProcessPipe = self.view.imageGalleryController.model.getSelectedProcessPipe()

            if selectedProcessPipe:         # check if a process pipe is selected

                # read original image
                img = hdrCore.image.Image.read(selectedProcessPipe.originalImage.path+'/'+selectedProcessPipe.originalImage.name)

                # resize
                screenY, screenX = self.hdrDisplay.model.displayModel['shape']  
                imgY, imgX,_ = img.shape

                marginY = int((screenY - imgY/2)/2)
                marginX = int(marginY/4)
                imgXp = int((screenX - 3*marginX)/2)
                img = img.process(hdrCore.processing.resize(),size=(None,imgXp))
                imgY, imgX, _ = img.shape

                # original image after resize
                ori = copy.deepcopy(img)

                # build process pipe from selected one them compute
                pp = hdrCore.processing.ProcessPipe()
                hdrCore.processing.ProcessPipe.autoResize = False   # stop autoResize
                params= []
                for p in selectedProcessPipe.processNodes: 
                    pp.append(copy.deepcopy(p.process),paramDict=None, name=copy.deepcopy(p.name))
                    params.append({p.name:p.params})
                img.metadata.metadata['processpipe'] = params
                pp.setImage(img)
                pp.compute()
                res = pp.getImage(toneMap=False)
                res = res.process(hdrCore.processing.clip())
                imgYres, imgXres, _ = res.colorData.shape

                hdrCore.processing.ProcessPipe.autoResize = True    # return to autoResize

                # make comparison image
                oriColorData = ori.colorData*self.hdrDisplay.model.displayModel['scaling']
                resColorData = res.colorData*self.hdrDisplay.model.displayModel['scaling']
                display = np.ones((screenY,screenX,3))*0.2
                marginY = int((screenY - imgY)/2)
                marginYres = int((screenY - imgYres)/2)

                display[marginY:marginY+imgY, marginX:marginX+imgX,:] = oriColorData
                display[marginYres:marginYres+imgYres, 2*marginX+imgX:2*marginX+imgX+imgXres,:] = resColorData
            
                # save as compOrigFinal.hdr
                colour.write_image(display,'compOrigFinal.hdr', method='Imageio')
                self.hdrDisplay.displayFile('compOrigFinal.hdr')
        else:
            self.view.statusBar().showMessage('stop assessment before') 

    def callBackExportHDR(self):
        """
        Callback of export HDR menu

        Export the image associated to selected process pipe
        """

        if pref.verbose:  print(" [CONTROL] >> AppController.callBackExportHDR()") 

        selectedProcessPipe = self.view.imageGalleryController.model.getSelectedProcessPipe()

        if selectedProcessPipe:
            # select dir where to save export
            self.dirName = QFileDialog.getExistingDirectory(None, 'Select Directory where to export HDR file', self.model.directory)

            self.view.statusBar().showMessage('exporting HDR image (HDR vesa 1000), full size image computation: start, please wait !')
            self.view.statusBar().repaint()
            # save current processpipe metada
            originalImage = copy.deepcopy(selectedProcessPipe.originalImage)
            originalImage.metadata.metadata['processpipe'] = selectedProcessPipe.toDict()
            originalImage.metadata.save()

            # load full size image
            img = hdrCore.image.Image.read(originalImage.path+'/'+originalImage.name)

            # turn off: autoResize
            hdrCore.processing.ProcessPipe.autoResize = False 
            # make a copy of selectedProcessPipe  
            processpipe = copy.deepcopy(selectedProcessPipe)

            # set image to process-pipe
            processpipe.setImage(img)

            # star parallel computation
            nbWidth,  nbHeight = 3,3
            thread.pCompute(self.callBackEndExportHDR, processpipe,nbWidth,nbHeight, toneMap=False, progress=self.view.statusBar().showMessage, meta=copy.deepcopy(img.metadata) )

    def callBackEndExportHDR(self, img, meta):
        # turn off: autoResize
        hdrCore.processing.ProcessPipe.autoResize = True  
        
        self.view.statusBar().showMessage('exporting HDR image (HDR vesa 1000), full size image computation: done !')

        # clip, scale
        img = img.process(hdrCore.processing.clip())
        img.colorData = img.colorData*hdrCore.utils.HDRdisplay['vesaDisplayHDR1000']['scaling']

        if self.dirName:
            pathExport = os.path.join(self.dirName, img.name[:-4]+hdrCore.utils.HDRdisplay['vesaDisplayHDR1000']['post']+'.hdr')
            img.type = hdrCore.image.imageType.HDR
            img.metadata = meta
            img.metadata.metadata['processpipe'] = None
            img.metadata.metadata['display'] =hdrCore.utils.HDRdisplay['vesaDisplayHDR1000']['tag']

            img.write(pathExport)

        colour.write_image(img.colorData,"temp.hdr", method='Imageio') # local copy for display
        self.hdrDisplay.displayFile("temp.hdr")

    def callBackExportAllHDR(self):
        if pref.verbose:  print(" [CONTROL] >> AppController.callBackExportAllHDR()")

        processPipes = self.view.imageGalleryController.getProcessPipes()

        # select dir where to save export
        dirName = QFileDialog.getExistingDirectory(None, 'Select Directory where to export HDR file', self.model.directory)
        self.view.statusBar().showMessage('exporting HDR vesa 1000 images ... please wait')
        self.view.statusBar().repaint()

        for pp_i in processPipes: res = pp_i.export(dirName,  size=None, to=hdrCore.utils.HDRdisplay['vesaDisplayHDR1000'],  progress = None)
# ------------------------------------------------------------------------------------------
# --- class MultiDockController() ----------------------------------------------------------
# ------------------------------------------------------------------------------------------
class MultiDockController():
    def __init__(self,parent=None, HDRcontroller = None):
        if pref.verbose: print(" [CONTROL] >> MultiDockController.__init__()")

        self.parent = parent
        self.view = view.MultiDockView(self, HDRcontroller)
        self.model = None


    def activateEDIT(self): self.switch(0)
    def activateINFO(self): self.switch(1)
    def activateIQA(self):  self.switch(2)

    def switch(self,nb):
        if pref.verbose:  print(" [CONTROL] >> MultiDockController.switch()")

        if not self.assessmentRunning(): self.view.switch(nb)
        else: self.parent.statusBar().showMessage('stop assessment before switching dock') 

    def setProcessPipe(self, processPipe): 
        if pref.verbose: print(" [CONTROL] >> MultiDockController.setProcessPipe(",processPipe.getImage().name,")")

        return self.view.setProcessPipe(processPipe)

    def assessmentRunning(self):
        return self.view.childControllers[2].assessmentRunning()
# ------------------------------------------------------------------------------------------
# ------------------------------------------------------------------------------------------
class EditImageController:

    def __init__(self, parent=None, HDRcontroller = None):
        if pref.verbose: print(" [CONTROL] >> EditImageController.__init__(",")")

        self.parent = parent

        self.previewHDR = True
        self.controllerHDR = HDRcontroller

        self.view = view.EditImageView(self)
        self.model = model.EditImageModel(self)

    def setProcessPipe(self, processPipe): 
        if pref.verbose: print(" [CONTROL] >> EditImageController.setProcessPipe(",")")

        if self.model.setProcessPipe(processPipe):

            # update view
            self.view.setProcessPipe(processPipe)
            self.view.imageWidgetController.setImage(processPipe.getImage())

            self.view.plotToneCurve()

            # update hdr viewer
            self.controllerHDR.displaySplash()

            return True
        else:
            return False

    def buildView(self,processPipe=None):
        if pref.verbose: print(" [CONTROL] >> EditImageController.buildView(",")")

        """ called when MultiDockController recall a controller/view """
        self.view = view.EditImageView(self, build=True)
        if processPipe: self.setProcessPipe(processPipe)

    def autoExposure(self): 
        if pref.verbose: print(" [CONTROL] >> EditImageController.autoExposure(",")")
        if self.model.processpipe:
      
            img = self.model.autoExposure()

            # update view with computed EV
            paramDict = self.model.getEV()
            self.view.exposure.setValue(paramDict['EV'])

            qPixmap =  self.view.setImage(img)
            self.parent.controller.parent.controller.view.imageGalleryController.setProcessPipeWidgetQPixmap(qPixmap)

    def changeExposure(self,value):
        if pref.verbose: print(" [CONTROL] >> EditImageController.changeExposure(",value,")")
        if self.model.processpipe: self.model.changeExposure(value)

    def changeContrast(self,value):
        if pref.verbose: print(" [CONTROL] >> EditImageController.changeContrast(",value,")")
        if self.model.processpipe: self.model.changeContrast(value)

    def changeToneCurve(self,controlPoints):
        if pref.verbose: print(" [CONTROL] >> EditImageController.changeToneCurve("")")
        if self.model.processpipe: self.model.changeToneCurve(controlPoints)

    def changeLightnessMask(self, maskValues):
        if pref.verbose: print(" [CONTROL] >> EditImageController.changeLightnessMask(",maskValues,")")
        if self.model.processpipe: self.model.changeLightnessMask(maskValues)

    def changeSaturation(self,value):
        if pref.verbose: print(" [CONTROL] >> EditImageController.changeSaturation(",value,")")
        if self.model.processpipe: self.model.changeSaturation(value)

    def changeColorEditor(self,values, idName):
        if pref.verbose: print(" [CONTROL] >> EditImageController.changeColorEditor(",values,")")
        if self.model.processpipe: self.model.changeColorEditor(values, idName)

    def changeGeometry(self,values):
        if pref.verbose: print(" [CONTROL] >> EditImageController.changeGeometry(",values,")")
        if self.model.processpipe: self.model.changeGeometry(values)

    def updateImage(self,imgTM):
        """
        updateImage: called when process-pipe computation is done
            
        Args:
            imgTM (hdrCoreimage.Image, required): tone mapped image (resized) for GUI display
        """
        qPixmap =  self.view.setImage(imgTM)
        self.parent.controller.parent.controller.view.imageGalleryController.setProcessPipeWidgetQPixmap(qPixmap)
        self.view.plotToneCurve()

        if self.previewHDR and self.model.autoPreviewHDR:
            self.controllerHDR.callBackUpdate()
# ------------------------------------------------------------------------------------------
class ImageInfoController:

    def __init__(self, parent=None):
        if pref.verbose: print(" [CONTROL] >> ImageInfoController.__init__()")

        self.parent = parent
        self.view = view.ImageInfoView(self)
        self.model = model.ImageInfoModel(self)

        self.callBackActive = True

    def setProcessPipe(self, processPipe): 
        if pref.verbose: print(" [CONTROL] >> ImageInfoController.setProcessPipe(",processPipe.getImage().name,")")
        self.model.setProcessPipe(processPipe)
        #### self.view.setImage(processPipe.getImage())
        self.view.setProcessPipe(processPipe)
        return True

    def buildView(self,processPipe=None):
        if pref.verbose: print(" [CONTROL] >> ImageInfoController.buildView()")

        """ called when MultiDockController recall a controller/view """
        self.view = view.ImageInfoView(self)
        if processPipe: self.setProcessPipe(processPipe)

    def useCaseChange(self,useCaseClass,useCase, on_off): 
        if pref.verbose: print(" [CONTROL] >> ImageInfoController.useCaseChange(",useCaseClass,",", useCase,",", on_off,")")
        self.model.changeUseCase(useCaseClass,useCase, on_off)
# ------------------------------------------------------------------------------------------
class ImageQualityController:
    def __init__(self, parent=None, HDRcontroller = None):
        if pref.verbose: print(" [CONTROL] >> ImageQualityController.__init__()")

        self.parent = parent
        self.model = model.ImageQualityModel(self)
        self.view = view.ImageQualityView(self)

        self.HDRcontroller = HDRcontroller

        self.running = False
        self.callBackActive = True

    def assessmentRunning(self): return self.running

    def setProcessPipe(self, processPipe): 
        if pref.verbose: print(" [CONTROL] >> ImageQualityController.setProcessPipe(",processPipe.getImage().name,")")

        pass

    def buildView(self,processPipe=None):
        if pref.verbose: print(" [CONTROL] >> ImageQualityController.buildView()")
        self.view = view.ImageQualityView(self)
        if processPipe: self.setProcessPipe(processPipe)

    def startStop(self): 

        if pref.verbose: print(" [CONTROL] >> ImageQualityController.startStop(",")")
        self.running = not self.running

        if self.running:

            if not self.model.userName:
                messageBox("uHDR: WARNING", "you must enter your name/pseudo!")
                self.running = False

            else:

                # start assessment: create vesaDisplayHDR1000 export images
                imgGalController = self.parent.controller.parent.imageGalleryController
                status = self.parent.controller.parent.statusBar()
                processPipes = imgGalController.getProcessPipes()

                if len(processPipes)==0:
                    messageBox("uHDR: WARNING", "you must select the directory with images to assess!")
                    self.running = False

                else:

                    localCopies = []

                    status.showMessage("exporting HDR images")
                    status.repaint()

                    for i,pp_i in enumerate(processPipes):

                        res = pp_i.export(None,  size=None, to=hdrCore.utils.HDRdisplay['vesaDisplayHDR1000'],  progress = None)
                        destFile = "temp_"+str(i)+".hdr"
                        colour.write_image(res.colorData,destFile, method='Imageio') # local copy for display
                        localCopies.append(destFile)
            
                    # start assessment 
                    self.model.processPipes = processPipes  # 
                    self.model.setImages(localCopies)       # send data to model
                    self.model.start()
                    self.model.next()                       # start
        else: 
            # stop assessment: save score, remove local copies
            self.model.save()
            self.model.resetImages()
            for localFile in self.model.localImageNames: os.remove(localFile)

    def next(self):
        if pref.verbose: print(" [CONTROL] >> ImageQualityController.next(",")")
        if self.assessmentRunning():
            q = self.model.next()
            self.setValues(q)

    def previous(self):
        if pref.verbose: print(" [CONTROL] >> ImageQualityController.previous(",")")
        if self.assessmentRunning():
            q = self.model.previous()
            self.setValues(q)

    def pseudo(self, name):
        if pref.verbose: print(" [CONTROL] >> ImageQualityController.pseudo(",name,")")

        if self.callBackActive:
            self.model.setPseudo(name)

    def score(self, label, value):
        if pref.verbose: print(" [CONTROL] >> ImageQualityController.score(",label,",", value,")")

        if self.callBackActive:
            self.model.setScore(label,value)

    def artifact(self, label, value):
        if pref.verbose: print(" [CONTROL] >> ImageQualityController.artifact(",label,",", value,")")

        if self.callBackActive:
            self.model.setArtifact(label,value)

    def setValues(self,q):
        if pref.verbose: print(" [CONTROL] >> ImageQualityController.setValues(",q,")")

        self.callBackActive = False
        self.view.setValues(q)
        self.callBackActive = True
# ------------------------------------------------------------------------------------------
class AdvanceSliderController():
    def __init__(self, parent,name, defaultValue, range, step,callBackValueChange=None,callBackAutoPush= None):
        if pref.verbose: print(" [CONTROL] >> AdvanceSliderController.__init__(",") ")
        self.parent = parent

        self.view = view.AdvanceSliderView(self,  name, defaultValue, range, step)
        self.model = model.AdvanceSliderModel(self, value=defaultValue)

        self.step = step
        self.defaultValue = defaultValue
        self.range = range

        self.callBackActive = True
        self.callBackValueChange = callBackValueChange
        self.callBackAutoPush = callBackAutoPush

    def sliderChange(self):

        value = self.view.slider.value()*self.step

        if pref.verbose: print(" [CB] >> AdvanceSliderController.sliderChange(",value,")[callBackActive:",self.callBackActive,"] ")

        self.model.value = value
        self.view.editValue.setText(str(value))
        if self.callBackActive and self.callBackValueChange: self.callBackValueChange(value)

    def setValue(self, value, callBackActive = True):
        if pref.verbose: print(" [CONTROL] >> AdvanceSliderController.setValue(",value,") ")

        """ set value value in 'model' range"""
        self.callBackActive = callBackActive
        self.view.slider.setValue(value/self.step)
        self.view.editValue.setText(str(value))
        self.model.setValue(value)

        self.callBackActive = True

    def reset(self):
        if pref.verbose : print(" [CB] >> AdvanceSliderController.reset(",") ")

        self.setValue(self.defaultValue,callBackActive = False)
        if self.callBackValueChange: self.callBackValueChange(self.defaultValue)

    def auto(self):
        if pref.verbose: print(" [CB] >> AdvanceSliderController.auto(",") ")

        if self.callBackAutoPush: self.callBackAutoPush()
# ------------------------------------------------------------------------------------------
class ToneCurveController():
    def __init__(self, parent):

        self.parent = parent
        self.model = model.ToneCurveModel()
        self.view = view.ToneCurveView(self)
        self.callBackActive = True

        # tone curve display control
        self.showInput =False
        self.showbefore = False
        self.showAfter = True

    def sliderChange(self, key, value):
        if pref.verbose: print(" [CB] >> ToneCurveController.sliderChange(",key,",",value,")[callBackActive:",self.callBackActive,"] ")

        if self.callBackActive:
            newValues = self.model.setValue(key, value, autoScale = False)
            self.parent.controller.changeToneCurve(newValues) 
  
            points = self.model.evaluate()

            self.callBackActive =  False

            self.view.sliderShadows.setValue(newValues["shadows"][1])
            self.view.editShadows.setText(str(newValues["shadows"][1]))

            self.view.sliderBlacks.setValue(newValues["blacks"][1])
            self.view.editBlacks.setText(str(newValues["blacks"][1]))

            self.view.sliderMediums.setValue(newValues["mediums"][1])
            self.view.editMediums.setText(str(newValues["mediums"][1]))

            self.view.sliderWhites.setValue(newValues["whites"][1])
            self.view.editWhites.setText(str(newValues["whites"][1]))

            self.view.sliderHighlights.setValue(newValues["highlights"][1])
            self.view.editHighlights.setText(str(newValues["highlights"][1]))

            self.callBackActive =  True

    def setValues(self, valuesDict,callBackActive = False):
        if pref.verbose: print(" [CONTROL] >> ToneCurveController.setValue(",valuesDict,") ")

        self.callBackActive = callBackActive

        self.model.setValues(valuesDict)
        points = self.model.evaluate()

        self.view.sliderShadows.setValue(valuesDict["shadows"][1])
        self.view.editShadows.setText(str(valuesDict["shadows"][1]))

        self.view.sliderBlacks.setValue(valuesDict["blacks"][1])
        self.view.editBlacks.setText(str(valuesDict["blacks"][1]))

        self.view.sliderMediums.setValue(valuesDict["mediums"][1])
        self.view.editMediums.setText(str(valuesDict["mediums"][1]))

        self.view.sliderWhites.setValue(valuesDict["whites"][1])
        self.view.editWhites.setText(str(valuesDict["whites"][1]))

        self.view.sliderHighlights.setValue(valuesDict["highlights"][1])
        self.view.editHighlights.setText(str(valuesDict["highlights"][1]))

        self.callBackActive = True

    def reset(self, key):
        if pref.verbose: print(" [CONTROL] >> ToneCurveController.reset(",key,") ")

        valuesDefault = copy.deepcopy(self.model.default[key])[1]
        controls = self.model.setValue(key, valuesDefault)
        self.setValues(controls,callBackActive = False)

        self.parent.controller.changeToneCurve(controls) 

    def plotCurve(self):
        self.view.curve.plot([0,100],[0,100],'r--', clear=True)
        self.view.curve.plot([20,20],[0,100],'r--', clear=False)
        self.view.curve.plot([40,40],[0,100],'r--', clear=False)
        self.view.curve.plot([60,60],[0,100],'r--', clear=False)
        self.view.curve.plot([80,80],[0,100],'r--', clear=False)

        processPipe = self.parent.controller.model.getProcessPipe()
        idExposure = processPipe.getProcessNodeByName("tonecurve")

        bins = np.linspace(0,1,50+1)

        if self.showInput:
            imageInput = copy.deepcopy(processPipe.getInputImage())
            if imageInput.linear: imageInputColorData =colour.cctf_encoding(imageInput.colorData, function='sRGB')
            else: imageInputColorData = imageInput.colorData
            imageInputColorData[imageInputColorData>1]=1
            imageInputY = colour.sRGB_to_XYZ(imageInputColorData, apply_cctf_decoding=False)[:,:,1]
            nphistInput  = np.histogram(imageInputY, bins)[0]
            nphistInput  = nphistInput/np.amax(nphistInput)
            self.view.curve.plot(bins[:-1]*100,nphistInput*100,'k--',  clear=False)

        if self.showbefore:
            imageBeforeColorData = processPipe.processNodes[idExposure-1].outputImage.colorData
            imageBeforeColorData[imageBeforeColorData>1]=1
            imageBeforeY = colour.sRGB_to_XYZ(imageBeforeColorData, apply_cctf_decoding=False)[:,:,1]
            nphistBefore  = np.histogram(imageBeforeY, bins)[0]
            nphistBefore  = nphistBefore/np.amax(nphistBefore)
            self.view.curve.plot(bins[:-1]*100,nphistBefore*100,'b--',  clear=False)

        if self.showAfter:
            imageAftercolorData = processPipe.processNodes[idExposure].outputImage.colorData
            imageAftercolorData[imageAftercolorData>1]=1
            imageAfterY  = colour.sRGB_to_XYZ(imageAftercolorData,  apply_cctf_decoding=False)[:,:,1]
            nphistAfter   = np.histogram(imageAfterY, bins)[0]
            nphistAfter   =nphistAfter/np.amax(nphistAfter)
            self.view.curve.plot(bins[:-1]*100,nphistAfter*100,'b',     clear=False)

        controlPointCoordinates= np.asarray(list(self.model.control.values()))
        self.view.curve.plot(controlPointCoordinates[1:-1,0],controlPointCoordinates[1:-1,1],'ro', clear=False)
        points = np.asarray(self.model.curve.evalpts)
        x = points[:,0]
        self.view.curve.plot(points[x<100,0],points[x<100,1],'r',clear=False)
# ------------------------------------------------------------------------------------------
class LightnessMaskController():
    def __init__(self, parent):
        if pref.verbose: print(" [CONTROL] >> MaskLightnessController.__init__(",")")

        self.parent= parent
        self.model = model.LightnessMaskModel(self)
        self.view = view.LightnessMaskView(self)

        self.callBackActive = True

    def maskChange(self,key, on_off):
        if pref.verbose: print(" [CB] >> MaskLightnessController.maskChange(",key,",",on_off,")[callBackActive:",self.callBackActive,"] ")

        maskState = self.model.maskChange(key, on_off)  
        self.parent.controller.changeLightnessMask(maskState) 

    def setValues(self, values,callBackActive = False):
        if pref.verbose: print(" [CONTROL] >> LightnessMaskController.setValue(",values,") ")

        self.callBackActive = callBackActive

        self.model.setValues(values)

        self.view.checkboxShadows.setChecked(values["shadows"])
        self.view.checkboxBlacks.setChecked(values["blacks"])
        self.view.checkboxMediums.setChecked(values["mediums"])
        self.view.checkboxWhites.setChecked(values["whites"])
        self.view.checkboxHighlights.setChecked(values["highlights"])   

        self.callBackActive = True
# ------------------------------------------------------------------------------------------
class HDRviewerController():
    def __init__(self, parent):
        if pref.verbose: print(" [CONTROL] >> HDRviewerController.__init__(",")")

        self.parent= parent
        self.model = model.HDRviewerModel(self)
        self.view = None 

        self.viewerProcess = None

        self.displaySplash()

    def setView(self, view): self.view = view

    def callBackUpdate(self):
        selectedProcessPipe = self.parent.view.imageGalleryController.model.getSelectedProcessPipe()
        img = selectedProcessPipe.getImage(toneMap = False)
        self.displayIMG(img)
        self.model.currentIMG = img

    def callBackAuto(self,on_off):
        self.parent.view.dock.view.childControllers[0].model.autoPreviewHDR = on_off

    def callBackCompare(self):
        if self.model.currentIMG:
            old = self.model.currentIMG
            old = old.process(hdrCore.processing.clip())

            sp = self.parent.view.imageGalleryController.model.getSelectedProcessPipe()
            img = sp.getImage(toneMap = False)
            img = img.process(hdrCore.processing.clip())


            h1, w1, _ = old.colorData.shape
            h2, w2, _ = img.colorData.shape
            hD, wD = self.model.displayModel['shape']
            hM = int((hD - max(h1,h2))/2)
            wM = int((wD - (w1+w2))/3)
            back =  np.ones((hD,wD,3))*0.2

            back[hM:hM+h1,wM:wM+w1,:] = old.colorData*self.model.displayModel['scaling']
            back[hM:hM+h2,2*wM+w1:2*wM+w1+w2,:] = img.colorData*self.model.displayModel['scaling']

            # save as temp.hdr
            colour.write_image(back,'temp.hdr', method='Imageio')
            self.displayFile('temp.hdr')

            self.model.currentIMG = img
        else: self.callBackUpdate()

    def displayFile(self, HDRfilename):
        """
        HDRviewerController display file: run HDRImageViewer process ti display HDR image (from filename)

        Args:
            HDRfilename: string
                Required  : hdr image filename
                
        """

         # check that no current display process already open
        if self.viewerProcess:
            # the display HDR process is already running
            # close current
            subprocess.run(['taskkill', '/F', '/T', '/IM', "HDRImageViewer*"],capture_output=False)
            time.sleep(0.05)
        # run display HDR process
        self.viewerProcess = subprocess.Popen(["HDRImageViewer.exe","-f", "-input:"+HDRfilename, "-f", "-h"], shell=True)
        time.sleep(0.10)
        psData = subprocess.run(['tasklist'], capture_output=True, universal_newlines=True).stdout
        if not 'HDRImageViewer' in psData: 
            # re-run display HDR process
            self.viewerProcess = subprocess.Popen(["HDRImageViewer.exe","-f", "-input:"+HDRfilename, "-f", "-h"], shell=True)

    def displayIMG(self, img):
        img = img.process(hdrCore.processing.clip())
        colorData = img.colorData*self.model.displayModel['scaling']

        h,w, _ = colorData.shape
        hD, wD = self.model.displayModel['shape']

        if w<wD:
            back = np.ones((hD,wD,3))*0.2
            marginW = int((wD-w)/2)
            marginH = int((hD-h)/2)

            back[marginH:marginH+h,marginW:marginW+w,:]=colorData

        # save as temp.hdr
        colour.write_image(back,'temp.hdr', method='Imageio')
        self.displayFile('temp.hdr')

    def displaySplash(self):
        self.model.currentIMG = None
        self.displayFile('grey.hdr')

    def close(self):
        # check that no current display process already open
        if self.viewerProcess:
            # the display HDR process is already running
            # close current
            subprocess.run(['taskkill', '/F', '/T', '/IM', "HDRImageViewer*"],capture_output=False)
            self.viewerProcess = None
# ------------------------------------------------------------------------------------------
class LchColorSelectorController:
    def __init__(self, parent, idName = None):
        if pref.verbose: print(" [CONTROL] >> LchColorSelectorController.__init__(",") ")
        self.parent = parent
        self.model =    model.LchColorSelectorModel(self)
        self.view =     view.LchColorSelectorView(self)

        self.idName = idName

        self.callBackActive = True

    def sliderHueChange(self, vMin, vMax):
        values  = self.model.setHueSelection(vMin,vMax)
        if self.callBackActive : self.parent.controller.changeColorEditor(values, self.idName)

    def sliderChromaChange(self, vMin, vMax):
        values  = self.model.setChromaSelection(vMin,vMax)
        if self.callBackActive : self.parent.controller.changeColorEditor(values, self.idName)

    def sliderLightnessChange(self, vMin, vMax):
        values  = self.model.setLightnessSelection(vMin,vMax)
        if self.callBackActive : self.parent.controller.changeColorEditor(values, self.idName)

    def sliderExposureChange(self, ev):
        values = self.model.setExposure(ev)
        if self.callBackActive : self.parent.controller.changeColorEditor(values, self.idName)

    def sliderSaturationChange(self, sat):
        values = self.model.setSaturation(sat)
        if self.callBackActive : self.parent.controller.changeColorEditor(values, self.idName)

    def sliderContrastChange(self, cc):
        values = self.model.setContrast(cc)
        if self.callBackActive : self.parent.controller.changeColorEditor(values, self.idName)

    def sliderHueShiftChange(self, hs):
        values = self.model.setHueShift(hs)
        if self.callBackActive : self.parent.controller.changeColorEditor(values, self.idName)

    def checkboxMaskChange(self,value): 
        values = self.model.setMask(value)
        if self.callBackActive : self.parent.controller.changeColorEditor(values, self.idName)

    def setValues(self, values, callBackActive = False):
        if pref.verbose: print(" [CONTROL] >> LchColorSelectorController.setValue(",values,") ")

        self.callBackActive = callBackActive
        # slider hue selection
        v = values['selection']['hue'] if 'hue' in values['selection'].keys() else (0,360)
        self.view.sliderHueMin.setValue(v[0])
        self.view.sliderHueMax.setValue(v[1])

        # slider chroma selection
        v = values['selection']['chroma'] if 'chroma' in values['selection'].keys() else (0,100)
        self.view.sliderChromaMin.setValue(v[0])
        self.view.sliderChromaMax.setValue(v[1])

        # slider lightness
        v = values['selection']['lightness'] if 'lightness' in values['selection'].keys() else (0,100)
        self.view.sliderLightMin.setValue(v[0]*3)
        self.view.sliderLightMax.setValue(v[1]*3)

        # hue shift editor
        v = values['edit']['hue'] if 'hue' in values['edit'].keys() else 0
        self.view.sliderHueShift.setValue(v)
        self.view.valueHueShift.setText(str(v)) 

        # exposure editor
        v = values['edit']['exposure'] if 'exposure' in values['edit'].keys() else 0
        self.view.sliderExposure.setValue(v*30)
        self.view.valueExposure.setText(str(v)) 

        # contrast editor
        v = values['edit']['contrast'] if 'contrast' in values['edit'].keys() else 0
        self.view.sliderContrast.setValue(v)
        self.view.valueContrast.setText(str(v))  

        # saturation editor
        v = values['edit']['saturation'] if 'saturation' in values['edit'].keys() else 0
        self.view.sliderSaturation.setValue(v)
        self.view.valueSaturation.setText(str(v))  

        # mask
        v = values['mask'] if 'mask' in values.keys() else False
        self.view.checkboxMask.setChecked(values['mask'])             

        self.model.setValues(values)

        self.callBackActive = True
# ------------------------------------------------------------------------------------------
class GeometryController:
    def __init__(self, parent, ):
        if pref.verbose: print(" [CONTROL] >> GeometryController.__init__(",") ")
        self.parent = parent
        self.model =    model.GeometryModel(self)
        self.view =     view.GeometryView(self)

        self.callBackActive = True
    # callbacks
    def sliderCroppingVerticalAdjustementChange(self,v):
        values = self.model.setCroppingVerticalAdjustement(v)
        if self.callBackActive : self.parent.controller.changeGeometry(values)

    def sliderRotationChange(self,v):
        values = self.model.setRotation(v)
        if self.callBackActive : self.parent.controller.changeGeometry(values)

    def setValues(self, values, callBackActive = False):
        if pref.verbose: print(" [CONTROL] >> GeometryController.setValue(",values,") ")

        up =        values['up']        if 'up' in values.keys()        else 0.0
        rotation =  values['rotation']  if 'rotation' in values.keys()  else 0.0

        self.callBackActive = callBackActive

        self.view.sliderCroppingVerticalAdjustement.setValue(up)
        self.view.sliderRotation.setValue(rotation*6)
       
        self.model.setValues(values)

        self.callBackActive = True

# ------------------------------------------------------------------------------------------
# ------------------------------------------------------------------------------------------
# message widget functions
# ------------------------------------------------------------------------------------------
# ------------------------------------------------------------------------------------------

def messageBox(title, text):
        msg = QMessageBox()
        msg.setText(text)
        msg.setStandardButtons(QMessageBox.Ok)
        msg.setWindowTitle(title)
        msg.setStandardButtons(QMessageBox.Ok)
        msg.setEscapeButton(QMessageBox.Close)
        msg.exec_()
def okCancelBox(title, text):
        msg = QMessageBox()
        msg.setText(text)
        msg.setWindowTitle(title)
        msg.setStandardButtons(QMessageBox.Ok | QMessageBox.Cancel)
        msg.setEscapeButton(QMessageBox.Close)
        return msg.exec_()
# ------------------------------------------------------------------------------------------

