#!/usr/bin/env python
# encoding: utf-8
"""
The model portion of the glance GUI code. 

Created by evas Oct 2011.
Copyright (c) 2011 University of Wisconsin SSEC. All rights reserved.
"""

# these first two lines must stay before the pylab import
import matplotlib
matplotlib.use('Qt4Agg') # use the Qt Anti-Grain Geometry rendering engine

from pylab import *

import matplotlib.cm     as cm
import matplotlib.pyplot as plt
import matplotlib.colors as colors

import sys, os.path, logging
import numpy as np

import glance.data    as dataobjects
import glance.figures as figures
import glance.io      as io

LOG = logging.getLogger(__name__)

"""
The model handles the data in the Glance GUI. It not only stores data and handles
updates, it's also responsible for caching data and handling some logic related to
what data is used with overrides. 

The model allows outside objects to register to listen for data updates or for
errors. It's expected that error handlers will manage either logging or displaying
errors appropriately. 
"""

# constants for the possible image types
ORIGINAL_A = "Original A Data"
ORIGINAL_B = "Original B Data"
ABS_DIFF   = "Abs. Difference"
RAW_DIFF   = "Raw Difference"
HISTOGRAM  = "Histogram"
MISMATCH   = "Mismatch Areas"
SCATTER    = "Scatter Plot"
HEX_PLOT   = "Hex Plot"

# a list of all the image types, for convenience
IMAGE_TYPES = [ORIGINAL_A,
               ORIGINAL_B,
               ABS_DIFF,
               RAW_DIFF,
               HISTOGRAM,
               MISMATCH,
               SCATTER,
               HEX_PLOT
              ]

# the number of bins to use for histograms
DEFAULT_NUM_BINS = 50

class _FileModelData (object) :
    """
    This object is meant to be used internally by the GUI model. The model is going to mess with the
    data directly. This is not the best practice ever, but I like it more than using a dictionary straight.
    This data object includes the following:
    
    self.file             - the FileInfo object representing this file, can be used to load more information later
    self.variable         - the name of the selected variable
    self.doOverride       - false if the default fill value should be used, true otherwise
    self.fillValue        - the fill value (may be different than the default), should be used when override is true
    self.defaultFillValue - the default fill value, should be used when override is false
    self.ALL_VARIABLES    - a list of all the variable names in the file
    self.var_data         - the data contained in the currently selected variable (or None if no variable is selected)
    self.var_attrs        - a dictionary of the variable attributes, keyed with the attribute names and containing their values
    
    TODO, eventually replace this with a better data object and/or some combination of data and settings objects?
    """
    
    def __init__(self, file_object=None, variable_selection=None, do_override=False, fill_value=None, default_fill_value=None,
                 variables_list=None, variable_data=None, variable_attributes={ }) :
        """
        create a set of model data, using the data passed in
        
        Note: the file_object is intended to be a FileInfo object from glance.data
        """
        
        self.file             = file_object
        self.variable         = variable_selection
        self.doOverride       = do_override
        self.fillValue        = fill_value
        self.defaultFillValue = default_fill_value
        self.ALL_VARIABLES    = variables_list
        
        self.var_data         = variable_data
        self.var_attrs        = variable_attributes

class GlanceGUIModel (object) :
    """
    This is the main model that handles the information behind the glance GUI.
    It includes:
    
    self.fileData      - a dictionary with _FileModelData objects indexed by the two file prefixes
    self.epsilon       - the epsilon value for comparison
    self.imageType     - the image type that should be created when files are compared
    self.dataListeners - objects that want to be notified when data changes
    self.errorHandlers - objects that want to be notified when there's a serious error
    """
    
    def __init__ (self) :
        """
        set up the basic model with our initial default values and empty listener lists
        """
        
        # set up the file related data structures
        self.fileData      = { }
        self.fileData["A"] = _FileModelData( )
        self.fileData["B"] = _FileModelData( )
        
        # general settings
        self.epsilon       = 0.0
        self.imageType     = None
        
        # select the first image type as a default
        self.imageType     = IMAGE_TYPES[0]
        
        # this represents all the people who want to hear about data updates
        # these people can register and will get data related messages
        self.dataListeners = [ ]
        # this represents all the people who want to hear when we have errors
        # that the user should know about
        # these people can register and will get error related messages
        self.errorHandlers = [ ]
    
    def loadNewFile (self, filePrefix, newFilePath) :
        """
        load up a new file based on the prefix and path given
        """
        
        # check to see if we were given a valid file path
        if (newFilePath is None) or (len(newFilePath) is 0) :
            LOG.debug("No file selected. Aborting load.")
            return # if there's no path, we can't load anything
        
        # attempt to open the file
        try :
            newFile      = dataobjects.FileInfo(str(newFilePath))
        except KeyError :
            newFile = None
            messageTemp = "Unable to open file. Glance was not able to determine the file type for\n " + newFilePath + "\nor cannot process that type of file."
            for errorHandler in self.errorHandlers :
                errorHandler.handleWarning(messageTemp)
        
        # if we couldn't get a valid file, stop now
        if newFile is None :
            return
        
        # get the list of variables, and pick one
        variableList = sorted(newFile.file_object()) # gets a list of all the variables in the file
        tempVariable = variableList[0]
        fillValue    = newFile.file_object.missing_value(tempVariable)
        
        # save all of the data related to this file for later use
        self.fileData[filePrefix].file               = newFile
        self.fileData[filePrefix].variable           = tempVariable
        self.fileData[filePrefix].doOverride         = False
        self.fileData[filePrefix].defaultFillValue   = fillValue
        self.fileData[filePrefix].fillValue          = fillValue
        self.fileData[filePrefix].ALL_VARIABLES      = variableList
        
        # get the size of the currently selected variable TODO, is it possible to do this without loading the variable?
        tempShape = self._load_variable_data(filePrefix, str(self.fileData[filePrefix].variable))
        
        # get the variable's attributes TODO, does this work on all types of files? (FIXME no, make a general method in io!)
        self.fileData[filePrefix].var_attrs          = newFile.file_object.get_variable_object(str(tempVariable)).attributes()
        
        # Now tell our data listeners that the file data changed
        for dataListener in self.dataListeners :
            LOG.debug("Sending update for file " + filePrefix + " with loaded data.")
            dataListener.fileDataUpdate(filePrefix, newFile.path, tempVariable, False, fillValue, str(tempShape),
                                        variable_list=variableList, attribute_list=self.fileData[filePrefix].var_attrs)
    
    def _load_variable_data (self, file_prefix, variable_name) :
        """
        Load up new variable data, saving it to our fileData structure
        return the shape of the data for convenience
        
        TODO, can this be handled as a background task in the future?
        """
        
        self.fileData[file_prefix].var_data = self.fileData[file_prefix].file.file_object[variable_name]
        return self.fileData[file_prefix].var_data.shape
    
    def sendGeneralSettingsData (self) :
        """
        send off the general settings data that's not related to the individual files
        """
        
        # let each of our listeners know about the general data
        for dataListener in self.dataListeners :
            dataListener.updateEpsilon(self.epsilon)
            dataListener.updateImageTypes(self.imageType, list=IMAGE_TYPES)
    
    def updateFileDataSelection (self, file_prefix, newVariableText=None, newOverrideValue=None, newFillValue=np.nan) :
        """
        someone has updated one or more of the file related data selections
        
        Note: if an input value is left at it's default (None or nan) then it's assumed that it was not externally changed
        """
        
        didUpdate = False
        
        # update the variable selection if needed
        if (newVariableText is not None) and (newVariableText != self.fileData[file_prefix].variable) :
            if newVariableText in self.fileData[file_prefix].ALL_VARIABLES :
                LOG.debug("Setting file " + file_prefix + " variable selection to: " + newVariableText)
                self.fileData[file_prefix].variable = newVariableText
                didUpdate = True
                
                # load the data for this new variable
                self._load_variable_data(file_prefix, str(newVariableText))
                
                # get the variable's attributes TODO, does this work on all types of files? (FIXME nope, make a general method in io!)
                self.fileData[file_prefix].var_attrs = self.fileData[file_prefix].file.file_object.get_variable_object(str(newVariableText)).attributes()
                
                # the new fill value should be loaded and the override should be cleared
                self.fileData[file_prefix].doOverride        = False
                self.fileData[file_prefix].defaultFillValue  = self.fileData[file_prefix].file.file_object.missing_value(str(newVariableText))
                self.fileData[file_prefix].fillValue         = self.fileData[file_prefix].defaultFillValue
        
        # update the override selection if needed
        if newOverrideValue is not None :
            LOG.debug("Setting file " + file_prefix + " override selection to: " + str(newOverrideValue))
            self.fileData[file_prefix].doOverride = newOverrideValue
            didUpdate = True
        
        # update the fill value if needed
        if newFillValue is not np.nan :
            LOG.debug("Setting file " + file_prefix + " fill value to: " + str(newFillValue))
            self.fileData[file_prefix].fillValue = newFillValue
            didUpdate = True
        
        # let our data listeners know about any changes
        if didUpdate :
            for listener in self.dataListeners :
                listener.fileDataUpdate(file_prefix, self.fileData[file_prefix].file.path,  self.fileData[file_prefix].variable,
                                                     self.fileData[file_prefix].doOverride, self._select_fill_value(file_prefix),
                                                     str(self.fileData[file_prefix].var_data.shape), attribute_list=self.fileData[file_prefix].var_attrs)
    
    def _select_fill_value (self, file_prefix) :
        """
        which fill value should currently be used?
        """
        return self.fileData[file_prefix].fillValue if self.fileData[file_prefix].doOverride else self.fileData[file_prefix].defaultFillValue
    
    def updateSettingsDataSelection (self, newEpsilonValue=np.nan, newImageType=None) :
        """
        someone has changed one or more of the general settings related data values
        
        Note: if an input value is left at it's default (None or nan) then it's assumed that it was not externally changed
        """
        
        didUpdate = False
        
        # update the epsilon if needed
        if newEpsilonValue is not np.nan :
            LOG.debug("Setting epsilon to: " + str(newEpsilonValue))
            self.epsilon = newEpsilonValue
            didUpdate = True
        
        # update the image type if needed
        if (newImageType is not None) and (newImageType != self.imageType) :
            if newImageType in IMAGE_TYPES :
                LOG.debug("Setting image type to: " + newImageType)
                self.imageType = newImageType
                didUpdate = True
        
        # let our data listeners know about any changes
        if didUpdate :
            for listener in self.dataListeners :
                listener.updateEpsilon(self.epsilon)
                listener.updateImageTypes(self.imageType)
    
    def spawnPlotWithCurrentInfo (self) :
        """
        create a matplotlib plot using the current model information
        
        TODO, move this into some sort of figure manager model object/module?
        """
        
        LOG.info ("Preparing variable data for plotting...")
        
        aData = self.fileData["A"].var_data
        bData = self.fileData["B"].var_data
        
        message = None
        
        # TODO, right now this does not take into account the fact that the two "original" plots only need one of the two variables
        # minimally validate the data
        if (aData is None) or (bData is None) :
            message = ("Data for requested files was not available. " +
                       "Please load or reload files and try again.")
        # check to see if the two variables have the same shape of data
        elif aData.shape != bData.shape :
            message = (self.fileData["A"].variable + ' / ' + self.fileData["B"].variable + ' ' + 
                       'could not be compared because the data for these variables does not match in shape ' +
                       'between the two files (file A data shape: ' + str(aData.shape) + '; file B data shape: '
                       + str(bData.shape) + ').')
        # if the data isn't valid, stop now
        if message is not None :
            for errorHandler in self.errorHandlers :
                errorHandler.handleWarning(message)
            # we can't make any images from this data, so just return
            return
        
        tempAFillMask = np.zeros(aData.shape, dtype=np.bool)
        tempAFillMask[aData == float(self._select_fill_value("A"))] = True
        tempBFillMask = np.zeros(bData.shape, dtype=np.bool)
        tempBFillMask[bData == float(self._select_fill_value("B"))] = True
        tempCleanMask = tempAFillMask | tempBFillMask
        
        aDataClean = aData[~tempCleanMask]
        bDataClean = bData[~tempCleanMask]
        cleanMismatchMask = np.zeros(aDataClean.shape, dtype=np.bool)
        cleanMismatchMask[abs(aDataClean - bDataClean) > float(self.epsilon)] = True
        
        rawDiffDataClean = bDataClean - aDataClean
        
        # pull the units information
        aUnits = self.fileData["A"].file.file_object.get_attribute(str(self.fileData["A"].variable), io.UNITS_CONSTANT)
        bUnits = self.fileData["B"].file.file_object.get_attribute(str(self.fileData["B"].variable), io.UNITS_CONSTANT)
        
        LOG.info("Spawning plot window: " + self.imageType)
        
        plt.ion() # make sure interactive plotting is on
        
        # create the plot
        
        if   self.imageType == ORIGINAL_A :
            
            tempFigure = figures.create_simple_figure(aData, self.fileData["A"].variable + "\nin File A",
                                                      invalidMask=tempAFillMask, colorMap=cm.jet, units=aUnits)
            
        elif self.imageType == ORIGINAL_B :
            
            tempFigure = figures.create_simple_figure(bData, self.fileData["B"].variable + "\nin File B",
                                                      invalidMask=tempBFillMask, colorMap=cm.jet, units=bUnits)
            
        elif self.imageType == ABS_DIFF :
            
            tempFigure = figures.create_simple_figure(np.abs(bData - aData), "Absolute value of difference\nin " + self.fileData["A"].variable,
                                                      invalidMask=tempCleanMask, colorMap=cm.jet, units=aUnits)
            
        elif self.imageType == RAW_DIFF :
            
            tempFigure = figures.create_simple_figure(bData - aData, "Value of (Data File B - Data File A)\nfor " + self.fileData["A"].variable,
                                                      invalidMask=tempCleanMask, colorMap=cm.jet, units=aUnits)
            
        elif self.imageType == HISTOGRAM :
            
            tempFigure = figures.create_histogram(rawDiffDataClean, DEFAULT_NUM_BINS, "Difference in\n" + self.fileData["A"].variable,
                                                  "Value of (B - A) at each data point", "Number of points with a given difference", units=aUnits)
            
        elif self.imageType == MISMATCH :
            
            mismatchMask = np.zeros(aData.shape, dtype=bool)
            mismatchMask[abs(aData - bData) > float(self.epsilon)] = True
            mismatchMask[tempCleanMask] = False
            mismatchMask[tempBFillMask ^ tempAFillMask] = True
            
            tempFigure = figures.create_simple_figure(aData, "Areas of mismatch data\nin " + self.fileData["A"].variable,
                                                      invalidMask=tempCleanMask, tagData=mismatchMask, colorMap=figures.MEDIUM_GRAY_COLOR_MAP, units=aUnits)
            
        elif self.imageType == SCATTER :
            
            figures.create_scatter_plot(aDataClean, bDataClean, "Value in File A vs Value in File B", 
                                        "File A Value in " + self.fileData["A"].variable,
                                        "File B Value in " + self.fileData["B"].variable,
                                        badMask=cleanMismatchMask, epsilon=float(self.epsilon),
                                        units_x=aUnits, units_y=bUnits)
            
        elif self.imageType == HEX_PLOT :
            
            tempFigure = figures.create_hexbin_plot(aDataClean, bDataClean,
                                                    "Value in File A vs Value in File B",
                                                    "File A Value in " + self.fileData["A"].variable,
                                                    "File B Value in " + self.fileData["B"].variable,
                                                    epsilon=float(self.epsilon),
                                                    units_x=aUnits, units_y=bUnits)
        
        plt.draw()
    
    def registerDataListener (self, objectToRegister) :
        """
        add the given object to our list of data listeners
        """
        
        if objectToRegister not in self.dataListeners :
            self.dataListeners.append(objectToRegister)
    
    def registerErrorHandler (self, objectToRegister) :
        """
        add the given object to our list of error handlers
        """
        
        if objectToRegister not in self.errorHandlers :
            self.errorHandlers.append(objectToRegister)

if __name__=='__main__':
    import doctest
    doctest.testmod()