# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements.  See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership.  The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License.  You may obtain a copy of the License at
#
#    http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied.  See the License for the
# specific language governing permissions and limitations
# under the License.

"""
Classes:
    Downscaling - Container for applying statistical downscaling.
"""


import numpy as np
from scipy.stats import linregress, percentileofscore


class Downscaling(object):
    """
    Statistical downscaling infers higher resolution information from lower resolution data.
    For example, data collected at a more coarse regional level applied to a more refined
    local level.

    Statistical downscaling establishes a relationship between different variables in the large scale
    and the local scale and applies that relationship to the local scale.
    """

    def __init__(self, ref_dataset, model_present, model_future):
        """ Default Downscaling constructor.

        :param ref_dataset: The Dataset to use as the reference dataset (observation)
        :type ref_dataset: Dataset
        :param model_present: model simulation to be compared with observation
        :type model_present: Dataset
        :param model_future: model simulation to be calibrated for prediction
        :type model_future: Dataset
        """
        self.ref_dataset = ref_dataset[~ref_dataset.mask].ravel()
        self.model_present = model_present.ravel()
        self.model_future = model_future.ravel()

    description = "statistical downscaling methods"

    def Delta_addition(self):
        """Calculate the mean difference between future and present simulation,
           then add the difference to the observed distribution

        :returns: downscaled model_present and model_future
        """
        ref = self.ref_dataset
        model_present = self.model_present
        model_future = self.model_future

        return model_present, ref + np.mean(model_future - model_present)

    def Delta_correction(self):
        """Calculate the mean difference between observation and present simulation,
           then add the difference to the future distribution

        :returns: downscaled model_present and model_future
        """
        ref = self.ref_dataset
        model_present = self.model_present
        model_future = self.model_future

        return model_present + np.mean(ref) - np.mean(model_present), model_future + \
            np.mean(ref) - np.mean(model_present)

    def Quantile_mapping(self):
        """Remove the biases for each quantile value

        Wood et al (2004) HYDROLOGIC IMPLICATIONS OF DYNAMICAL
        AND STATISTICAL APPROACHES TO DOWNSCALING CLIMATE MODEL OUTPUTS

        :returns: downscaled model_present and model_future
        """
        ref = self.ref_dataset
        model_present = self.model_present
        model_present_corrected = np.zeros(model_present.size)
        model_future = self.model_future
        model_future_corrected = np.zeros(model_future.size)

        for ival, model_value in enumerate(model_present):
            percentile = percentileofscore(model_present, model_value)
            model_present_corrected[ival] = np.percentile(ref, percentile)

        for ival, model_value in enumerate(model_future):
            percentile = percentileofscore(model_future, model_value)
            model_future_corrected[ival] = model_value + np.percentile(
                ref, percentile) - np.percentile(model_present, percentile)

        return model_present_corrected, model_future_corrected

    def Asynchronous_regression(self):
        """Remove the biases by fitting a linear regression model with ordered observational and
        model datasets

        Stoner et al (2013) An asynchronous regional regression model for statistical downscaling of
        daily climate variables

        :returns: downscaled model_present and model_future
        """

        ref_original = self.ref_dataset
        model_present = self.model_present
        model_present_sorted = np.sort(model_present)
        model_future = self.model_future

        # For linear regression, the size of reference data must be same as
        # model data.
        ref = np.zeros(model_present.size)

        for ival, model_value in enumerate(model_present_sorted):
            percentile = percentileofscore(model_present_sorted, model_value)
            ref[ival] = np.percentile(ref_original, percentile)

        slope, intercept = linregress(model_present_sorted, ref)[0:2]

        return model_present * slope + intercept, model_future * slope + intercept
