"""InaSAFE Disaster risk assessment tool developed by AusAid -
**IS Safe Interface.**

The purpose of the module is to centralise interactions between the gui
package and the underlying InaSAFE packages. This should be the only place
where SAFE modules are imported directly.

Contact : ole.moller.nielsen@gmail.com

.. note:: 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 2 of the License, or
     (at your option) any later version.

"""

__author__ = 'tim@linfiniti.com, ole.moller.nielsen@gmail.com'
__version__ = '0.5.2'
__revision__ = '6c4bbdad890a0592da6cee08599f1098071bd3f3'
__date__ = '04/04/2012'
__copyright__ = ('Copyright 2012, Australia Indonesia Facility for '
                 'Disaster Reduction')


# Standard modules
import os
import unicodedata
import logging

# SAFE functionality - passed on to QGIS modules
# pylint: disable=W0611
from safe.api import (get_admissible_plugins,
                      get_function_title,
                      get_plugins as safe_get_plugins,
                      read_keywords, bbox_intersection,
                      write_keywords as safe_write_keywords,
                      read_layer as safe_read_layer,
                      buffered_bounding_box,
                      verify as verify_util,
                      VerificationError,
                      temp_dir,
                      unique_filename,
                      safe_tr as safeTr,
                      calculate_impact as safe_calculate_impact,
                      BoundingBoxError,
                      get_plugins, get_version)
# pylint: enable=W0611

# InaSAFE GUI specific functionality
from PyQt4.QtCore import QCoreApplication
from safe_qgis.exceptions import (KeywordNotFoundException,
                                  StyleInfoNotFoundException,
                                  InvalidParameterException,
                                  InsufficientOverlapException)
LOGGER = logging.getLogger('InaSAFE')


def tr(theText):
    """We define a tr() alias here since the is_safe_interface implementation
    below is not a class and does not inherit from QObject.

    .. note:: see http://tinyurl.com/pyqt-differences

    Args:
       theText - string to be translated
    Returns:
       Translated version of the given string if available, otherwise
       the original string.
    """
    myContext = "is_safe_interface"
    return QCoreApplication.translate(myContext, theText)


def verify(theStatement, theMessage=None):
    """This is just a thin wrapper around safe.api.verify.

    Args:
        * theStatement - expression to verify
        * theMessage - message to display on failure
    Returns:
        None
    Raises:
        VerificationError
    """
    try:
        verify_util(theStatement, theMessage)
    except:
        raise


def getOptimalExtent(theHazardGeoExtent,
                     theExposureGeoExtent,
                     theViewportGeoExtent=None):
    """ A helper function to determine what the optimal extent is.
    Optimal extent should be considered as the intersection between
    the three inputs. The inasafe library will perform various checks
    to ensure that the extent is tenable, includes data from both
    etc.

    This is just a thin wrapper around safe.api.bbox_intersection.

    Typically the result of this function will be used to clip
    input layers to a commone extent before processing.

    Args:

        * theHazardGeoExtent - an array representing the hazard layer
           extents in the form [xmin, ymin, xmax, ymax]. It is assumed
           that the coordinates are in EPSG:4326 although currently
           no checks are made to enforce this.
        * theExposureGeoExtent - an array representing the exposure layer
           extents in the form [xmin, ymin, xmax, ymax]. It is assumed
           that the coordinates are in EPSG:4326 although currently
           no checks are made to enforce this.
        * theViewPortGeoExtent (optional) - an array representing the viewport
           extents in the form [xmin, ymin, xmax, ymax]. It is assumed
           that the coordinates are in EPSG:4326 although currently
           no checks are made to enforce this.

       ..note:: We do minimal checking as the inasafe library takes
         care of it for us.

    Returns:
       An array containing an extent in the form [xmin, ymin, xmax, ymax]
       e.g.::

          [100.03, -1.14, 100.81, -0.73]

    Raises:
        Any exceptions raised by the InaSAFE library will be propogated.
    """

    #
    myMessage = tr('theHazardGeoExtent or theExposureGeoExtent cannot be None.'
                   'Found: /ntheHazardGeoExtent: %s '
                    '/ntheExposureGeoExtent: %s' %
                   (theHazardGeoExtent, theExposureGeoExtent))

    if (theHazardGeoExtent is None) or (theExposureGeoExtent is None):
        raise BoundingBoxError(myMessage)

    # .. note:: The bbox_intersection function below assumes that
    #           all inputs are in EPSG:4326
    myOptimalExtent = bbox_intersection(theHazardGeoExtent,
        theExposureGeoExtent,
        theViewportGeoExtent)

    if myOptimalExtent is None:
        # Bounding boxes did not overlap
        myMessage = tr('Bounding boxes of hazard data, exposure data '
               'and viewport did not overlap, so no computation was '
               'done. Please make sure you pan to where the data is and '
               'that hazard and exposure data overlaps.')
        raise InsufficientOverlapException(myMessage)

    return myOptimalExtent


def getBufferedExtent(theGeoExtent, theCellSize):
    """Grow bounding box with one unit of resolution in each direction.

    Args:

        * theGeoExtent - Bounding box with format [W, S, E, N]
        * theCellSize - (resx, resy) Raster resolution in each direction.

        If resolution is None bbox is returned unchanged.

    Returns:
        Adjusted bounding box

    Raises:

        Any exceptions are propogated
    Note: See docstring for underlying function buffered_bounding_box
          for more details.
    """
    try:
        return buffered_bounding_box(theGeoExtent, theCellSize)
    except:
        raise


def availableFunctions(theKeywordList=None):
    """ Query the inasafe engine to see what plugins are available.

    Args:

       theKeywordList - an optional parameter which should contain
       a list of 2 dictionaries (the number of items in the list
       is not enforced). The dictionaries should be obtained by using
       readKeywordsFromFile e.g.::

           myFile1 = foo.shp
           myFile2 = bar.asc
           myKeywords1 = readKeywordsFromFile(myFile1)
           myKeywords2 = readKeywordsFromFile(myFile2)
           myList = [myKeywords1, myKeywords2]
           myFunctions = availableFunctions(myList)

    Returns:
       A dictionary of strings where each is a plugin name.

       .. note:: If theKeywordList is not provided, all available
        plugins will be returned in the list.

    Raises:
       NoFunctionsFoundException if no functions are found.
    """
    try:
        myDict = get_admissible_plugins(theKeywordList)
        #if len(myDict) < 1:
        #    myMessage = 'No InaSAFE impact functions could be found'
        #    raise NoFunctionsFoundException(myMessage)
        return myDict
    except:
        raise


def readKeywordsFromLayer(theLayer, keyword):
    """Get metadata from the keywords file associated with a layer.

    .. note:: Requires a inasafe layer instance as parameter.
    .. seealso:: getKeywordFromPath

    Args:

       * theLayer - a InaSAFE layer (vector or raster)
       * keyword - the metadata keyword to retrieve e.g. 'title'

    Returns:
       A string containing the retrieved value for the keyword.

    Raises:
       KeywordNotFoundException if the keyword is not recognised.
    """
    myValue = None
    if theLayer is None:
        raise InvalidParameterException()
    try:
        myValue = theLayer.get_keywords(keyword)
    except Exception, e:
        myMessage = tr('Keyword retrieval failed for %s (%s) \n %s' % (
                theLayer.get_filename(), keyword, str(e)))
        raise KeywordNotFoundException(myMessage)
    if not myValue or myValue == '':
        myMessage = tr('No value was found for keyword %s in layer %s' % (
                    theLayer.get_filename(), keyword))
        raise KeywordNotFoundException(myMessage)
    return myValue


def readKeywordsFromFile(theLayerPath, theKeyword=None):
    """Get metadata from the keywords file associated with a local
     file in the file system.

    .. note:: Requires a str representing a file path instance
              as parameter As opposed to readKeywordsFromLayer which
              takes a inasafe file object as parameter.

    .. seealso:: readKeywordsFromLayer

    Args:

       * theLayerPath - a string representing a path to a layer
           (e.g. '/tmp/foo.shp', '/tmp/foo.tif')
       * theKeyword - optional - the metadata keyword to retrieve e.g. 'title'

    Returns:
       A string containing the retrieved value for the keyword if
       the keyword argument is specified, otherwise the
       complete keywords dictionary is returned.

    Raises:
       KeywordNotFoundException if the keyword is not recognised.
    """
    # check the source layer path is valid
    if not os.path.isfile(theLayerPath):
        myMessage = tr('Cannot get keywords from a non-existent file.'
                       '%s does not exist.' % theLayerPath)
        raise InvalidParameterException(myMessage)

    # check there really is a keywords file for this layer
    myKeywordFilePath = os.path.splitext(theLayerPath)[0]
    myKeywordFilePath += '.keywords'
    if not os.path.isfile(myKeywordFilePath):
        wrappedPath = theLayerPath.replace(os.sep, '<wbr>' + os.sep)
        myMessage = tr('No keywords file found for %s' % wrappedPath)
        raise InvalidParameterException(myMessage)

    # now get the requested keyword using the inasafe library
    myDictionary = None
    try:
        myDictionary = read_keywords(myKeywordFilePath)
    except Exception, e:
        myMessage = tr('Keyword retrieval failed for %s (%s) \n %s' % (
                myKeywordFilePath, theKeyword, str(e)))
        raise KeywordNotFoundException(myMessage)

    # if no keyword was supplied, just return the dict
    if theKeyword is None:
        return myDictionary
    if not theKeyword in myDictionary:
        myMessage = tr('No value was found in file %s for keyword %s' % (
                    myKeywordFilePath, theKeyword))
        raise KeywordNotFoundException(myMessage)

    try:
        myValue = myDictionary[theKeyword]
    except:
        raise
    return myValue


def writeKeywordsToFile(theFilename, theKeywords):
    """Thin wrapper around the safe write_keywords function.

    Args:
        * thePath - str representing path to layer that must be written.
          If the file does not end in .keywords, its extension will be
          stripped off and the basename + .keywords will be used as the file.
        * theKeywords - a dictionary of keywords to be written
    Returns:
        None
    Raises:
        Any exceptions are propogated
    """
    myBasename, myExtension = os.path.splitext(theFilename)
    if 'keywords' not in myExtension:
        theFilename = myBasename + '.keywords'
    try:
        safe_write_keywords(theKeywords, theFilename)
    except:
        raise


def getStyleInfo(theLayer):
    """Get styleinfo associated with a layer.

    Args:

       * theLayer - InaSAFE layer (raster or vector)

    Returns:
       A list of dictionaries containing styleinfo info for a layer.

    Raises:

       * StyleInfoNotFoundException if the style is not found.
       * InvalidParameterException if the paramers are not correct.
    """

    if not theLayer:
        raise InvalidParameterException()

    if not hasattr(theLayer, 'get_style_info'):
        myMessage = tr('Argument "%s" was not a valid layer instance' %
               theLayer)
        raise StyleInfoNotFoundException(myMessage)

    try:
        myValue = theLayer.get_style_info()
    except Exception, e:
        myMessage = tr('Styleinfo retrieval failed for %s\n %s' % (
                    theLayer.get_filename(), str(e)))
        raise StyleInfoNotFoundException(myMessage)

    if not myValue or myValue == '':
        myMessage = tr('No styleInfo was found for layer %s' % (
                theLayer.get_filename()))
        raise StyleInfoNotFoundException(myMessage)
    return myValue


def makeAscii(x):
    """Convert QgsString to ASCII"""
    x = unicode(x)
    x = unicodedata.normalize('NFKD', x).encode('ascii', 'ignore')
    return x


def readSafeLayer(thePath):
    """Thin wrapper around the safe read_layer function.

    Args:
        thePath - str representing path to layer that must be opened.
    Returns:
        A safe readSafeLayer object is returned.
    Raises:
        Any exceptions are propogated
    """
    try:
        return safe_read_layer(makeAscii(thePath))
    except:
        raise


def getSafeImpactFunctions(theFunction=None):
    """Thin wrapper around the safe impact_functions function.

    Args:
        theFunction - optional str giving a specific plugins name that should
        be fetched.
    Returns:
        A safe impact function is returned
    Raises:
        Any exceptions are propogated
    """
    try:
        return safe_get_plugins(makeAscii(theFunction))
    except:
        raise


def getFunctionTitle(theFunction):
    """Thin wrapper around the safe get_function_title.

    Args:
        * theFunction - SAFE impact function instance to be used
    Returns:
        The title of a safe impact function is returned
    Raises:
        Any exceptions are propogated
    """

    # FIXME (Ole): I don't think we have to do try-except-raise.
    #              It would be the same just to call the function
    #              and let Python's normal exception handling
    #              propagate exceptions (just saving some lines and
    #              complexity)
    try:
        return get_function_title(theFunction)
    except:
        raise


def calculateSafeImpact(theLayers, theFunction):
    """Thin wrapper around the safe calculate_impact function.

    Args:
        * theLayers - a list of layers to be used. They should be ordered
          with hazard layer first and exposure layer second.
        * theFunction - SAFE impact function instance to be used
    Returns:
        A safe impact function is returned
    Raises:
        Any exceptions are propogated
    """
    try:
        return safe_calculate_impact(theLayers, theFunction)
    except:
        raise
