# Copyright (c) 2011-2014, B.I.Stepanov Institute of Physics, National Academy
# of Sciences of Belarus.
#
# 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.
#
# 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.
#
# You should have received a copy of the GNU General Public License along with
# this program; if not, write to the Free Software Foundation, Inc., 51
# Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.

import math
import numpy

from common.utils import txt

from common.DataRecord import *

__all__ = ['PolarOutput']

# *****************************************************************************
class PolarOutput(DataRecord):
    """Output data for aerosol backscatter and depolarization retrieval
    algorithm utilizing polarized lidar signals.

    Attributes to be read from a database:
      - 'latitude', 'longitude', 'altitude', 'startDate', 'stopDate',
        'startTime', 'stopTime': same as in 'PreparedLidarInput'.
      - 'retrievalDate', 'retrievalTime': date and time of the retrieval;
      - 'gridStep', 'gridZenithAngle': parameters of the data grid used for
        output data profiles (similar to those of 'PreparedLidarInput').

      - 'polarizationBase': polarization ID of the base lidar channel,
        (either 0 meaning "unpolarized" or 3 meaning "parallel-polarized").

      - 'backscatterParallel', 'backscatterCross': retrieved aerosol
        parallel-polarized and cross-polarized backscatter profiles.

      - 'measuredSignalBase', 'calculatedSignalBase': measured and calculated
        lidar signals, divided by their average values around the reference
        point, with molecular components removed.
      - 'measuredSignalCross', 'calculatedSignalCross': measured and calculated
        cross-polarized lidar signal ratio profiles.

      - 'signalId<channel>', where '<channel>' is either 'Base' or 'Cross':
        'localId' fields of the lidar inputs.

      - 'atmoModel': textual identifier for the molecular atmosphere (copied
        from the lidar database).
      - 'dispersion<channel>': Numpy array holding dispersion coefficients
        copied from the lidar inputs.

      - 'molBackscatter': molecular backscatter profile (copied from the lidar
        input).

      - 'weighting<channel>': weighting coefficients for lidar signals (copied
        from the algorithm params).
      - 'weightSmoothParallel', 'weightSmoothCross': weighting coefficients for
        smoothness terms of the retrieval equations (copied from the algorithm
        params).

      - 'parallelLeakage', 'molDepolarization', 'lidarRatio' parallel leakage
        of the cross-polarized lidar channel, molecular depolarization ratio,
        and aerosol lidar ratio (copied from the algorithm params).

      - 'aerBackscatterRatioRef': retrieved aerosol backscatter ratio at the
        reference point.
      - 'lidarCorrection': retrieved cross-polarized lidar correction factor.

      - 'molThicknessError<channel>', 'aerosolThicknessError<channel>',
        'totalBackscatterError<channel>': estimations of the atmospheric
        relative errors (copied from the corresponding 'PreparedLidarInput'
        instances).

      - 'residual<channel>': square root of the squared lidar equation
        residual, integrated over the retrieval interval.
      - 'resSmoothParallel', 'resSmoothCross': square root of the squared
        approximated second order derivative of the retrieved aerosol
        backscatter profiles, integrated over the retrieval interval.

    Attributes that are calculated manually in 'onDataLoaded':
      - 'gridHeightStep': height step of the data grid (similar to
        'PreparedLidarInput')."""

    # ---- Class attributes ---------------------------------------------------
    # Name of the aerosol profile output database table to read the data from.
    tableName = 'AerosolProfiles'

    fields = [
        DataField('latitude',  'Latitude',  'SINGLE'),
        DataField('longitude', 'Longitude', 'SINGLE'),
        DataField('altitude',  'Altitude',  'SINGLE'),

        DataField('startDate',      'StartDate',      'DATE'),
        DataField('stopDate',       'StopDate',       'DATE'),
        DataField('startTime',      'StartTime',      'TIME'),
        DataField('stopTime',       'StopTime',       'TIME'),
        DataField('retrievalDate',  'RetrievalDate',  'DATE'),
        DataField('retrievalTime',  'RetrievalTime',  'TIME'),

        DataField('gridStep',        'Step',      'SINGLE'),
        DataField('gridZenithAngle', 'Zenith',    'SINGLE'),

        DataField('polarizationBase', 'Polarization_Base', 'SMALLINT'),

        DataField('firstInputIndexBase', 'LeftBase', 'SMALLINT'),
        DataField('firstInputIndexCross', 'LeftCross', 'SMALLINT'),

        DataField('backscatterParallel', 'Backscatter_Parallel', 'LONGBINARY',
            numpy.float32),
        DataField('backscatterCross',    'Backscatter_Cross',    'LONGBINARY',
            numpy.float32),

        DataField('measuredSignalBase',  'Measured_Base',  'LONGBINARY',
            numpy.float32),
        DataField('measuredSignalCross', 'Measured_Cross', 'LONGBINARY',
            numpy.float32),

        DataField('calculatedSignalBase',  'Calculated_Base',  'LONGBINARY',
            numpy.float32),
        DataField('calculatedSignalCross', 'Calculated_Cross', 'LONGBINARY',
            numpy.float32),

        DataField('signalIdBase',  'IDLocal_Base',  'VARCHAR'),
        DataField('signalIdCross', 'IDLocal_Cross', 'VARCHAR'),

        DataField('atmosphereModel',  'AtmoModel',  'VARCHAR'),

        DataField('dispersionBase',  'StdDeviation_Base',  'LONGBINARY',
            numpy.float32),
        DataField('dispersionCross', 'StdDeviation_Cross', 'LONGBINARY',
            numpy.float32),

        DataField('molBackscatter', 'MolBackscatter', 'LONGBINARY',
            numpy.float32, required = False),
        DataField('aerBackscatter', 'AerBackscatter', 'LONGBINARY',
            numpy.float32, required = False),
        DataField('aerExtinction',  'AerExtinction', 'LONGBINARY',
            numpy.float32, required = False),

        DataField('weightingBase', 'Weight_Base', 'SINGLE'),
        DataField('weightingCross', 'Weight_Cross', 'SINGLE'),
        DataField('weightSmoothParallel', 'WeightSmooth_Parallel', 'SINGLE'),
        DataField('weightSmoothCross', 'WeightSmooth_Cross', 'SINGLE'),

        DataField('parallelLeakage', 'ParallelLeakage', 'SINGLE'),
        DataField('molDepolarization', 'MolDepolar', 'SINGLE'),
        DataField('lidarRatio', 'LidarRatio', 'SINGLE'),

        DataField('aerBackscatterRatioRef', 'AerRatioRef', 'SINGLE'),
        DataField('lidarCorrection', 'LidarCorrection', 'SINGLE'),

        DataField('molThicknessErrorBase', 'RE OMT_Base', 'SINGLE'),
        DataField('molThicknessErrorCross', 'RE OMT_Cross', 'SINGLE'),
        DataField('aerosolThicknessErrorBase', 'RE OAT_Base', 'SINGLE'),
        DataField('aerosolThicknessErrorCross', 'RE OAT_Cross', 'SINGLE'),
        DataField('totalBackscatterErrorBase', 'RE GBS_Base', 'SINGLE'),
        DataField('totalBackscatterErrorCross', 'RE GBS_Cross', 'SINGLE'),

        DataField('residualBase', 'Residual_Base', 'SINGLE'),
        DataField('residualCross', 'Residual_Cross', 'SINGLE'),
        DataField('resSmoothParallel', 'ResSmooth_Parallel', 'SINGLE'),
        DataField('resSmoothCross', 'ResSmooth_Cross', 'SINGLE'),

        DataField('backscatterDispersionParallel',
            'BackscatterDispersion_Parallel',
            'LONGBINARY', numpy.float32, required = False),
        DataField('depolarizationDispersion', 'DepolarizationDispersion',
            'LONGBINARY', numpy.float32, required = False),

        DataField('perturbanceEvaluations', 'PerturbanceEvaluations',
            'SMALLINT', required = False),

        DataField('whiteNoiseBase', 'WhiteNoise_Base', 'SINGLE',
            required = False),
        DataField('whiteNoiseCross', 'WhiteNoise_Cross', 'SINGLE',
            required = False),

        DataField('linearNoiseBase', 'LinearNoise_Base', 'SINGLE',
            required = False),
        DataField('linearNoiseCross', 'LinearNoise_Cross', 'SINGLE',
            required = False),
    ]

    createDatabaseErrorMessage = (
        'Depolarization retrieval output database file could not be created')
    openDatabaseErrorMessage = (
        'Depolarization retrieval output file is not a valid Access database '
        'or may not be opened for reading')
    queryFailureErrorMessage = (
        'Depolarization retrieval output: failed to query data from '
        'the database file')
    invalidFormatErrorPrefix = (
        'Depolarization retrieval output file format is invalid: ')
    writeDataErrorMessage = (
        'Failed to write the data to the depolarization retrieval output file')

    importDataFromExcelErrorPrefix = (
        'Failed to read the data from the Excel file')
    exportDataToExcelErrorPrefix = (
        'Failed to write the data to the Excel file')

    # ---- Public methods -----------------------------------------------------
    def onDataLoaded(self):
        # Calculate the vertical height step of the data grid.
        if self.gridStep is None or self.gridZenithAngle is None:
            self.gridHeigthStep = None
        else:
            self.gridHeightStep = (self.gridStep *
                math.cos(self.gridZenithAngle * math.pi / 180.0))

    def getErrorMessage(self):
        """Return an error message describing the malformed data or 'None' if
        all the attribute values are plausible and consistent."""

        # Check if some of the values were missing in the database.
        for name in self.getRequiredAttributeNames():
            if getattr(self, name) is None:
                return ('%s database field contains no data' %
                    txt.quote(self.getFieldName(name)))
