# Copyright [2024] Expedia, Inc.
#
# Licensed 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.

# pylint: disable=unused-argument
# pylint: disable=invalid-name
# pylint: disable=too-many-ancestors
# pylint: disable=no-member
from typing import List, Optional, Union

import pyspark.sql.functions as F
import tensorflow as tf
from pyspark import keyword_only
from pyspark.ml.param import Param, Params, TypeConverters
from pyspark.sql import DataFrame
from pyspark.sql.types import DataType

from kamae.spark.params import SingleInputSingleOutputParams
from kamae.spark.utils import single_input_single_output_scalar_transform
from kamae.tensorflow.layers import ImputeLayer

from .base import BaseTransformer


class ImputeParams(Params):
    """
    Mixin class used to provide imputation and mask value needed for imputation.
    """

    imputeValue = Param(
        Params._dummy(),
        "imputeValue",
        "Value to be imputed.",
        typeConverter=TypeConverters.identity,
    )

    maskValue = Param(
        Params._dummy(),
        "maskValue",
        """
        Value which is to be replaced with the imputation value.
        Value is ignored when computing the imputation value.
        """,
        typeConverter=TypeConverters.identity,
    )

    def setImputeValue(self, value: Union[float, int, str]) -> "ImputeParams":
        """
        Sets the parameter imputeValue to the given scalar value.

        :param value: List of imputeValues values.
        :returns: Instance of class mixed in.
        """
        if value is None:
            raise ValueError("Impute value cannot be None")
        return self._set(imputeValue=value)

    def getImputeValue(self) -> Union[float, int, str]:
        """
        Gets the imputeValue parameter.

        :returns: String, float or int imputeValue
        """
        return self.getOrDefault(self.imputeValue)

    def setMaskValue(self, value: Union[float, int, str]) -> "ImputeParams":
        """
        Sets the maskValue parameter.

        :param value: Float value to use as the mask value.
        :returns: Instance of class mixed in.
        """
        return self._set(maskValue=value)

    def getMaskValue(self) -> Union[float, int, str]:
        """
        Gets the maskValue parameter.
        :returns: String, float or int value of the mask value.
        """
        return self.getOrDefault(self.maskValue)


class ImputeTransformer(BaseTransformer, ImputeParams, SingleInputSingleOutputParams):
    """
    Imputation transformer for use in Spark pipelines.
    This is used to impute the mean or median value when
    value is null or equalling a mask
    """

    @keyword_only
    def __init__(
        self,
        inputCol: Optional[str] = None,
        outputCol: Optional[str] = None,
        inputDtype: Optional[str] = None,
        outputDtype: Optional[str] = None,
        layerName: Optional[str] = None,
        imputeValue: Optional[Union[float, int, str]] = None,
        maskValue: Optional[Union[float, int, str]] = None,
    ) -> None:
        """
        Initializes an ImputeTransformer transformer.

        :param inputCol: Input column name to impute over.
        :param outputCol: Output column name.
        :param inputDtype: Input data type to cast input column to before
        transforming.
        :param outputDtype: Output data type to cast the output column to after
        transforming.
        :param layerName: Name of the layer. Used as the name of the tensorflow layer
        in the keras model.
        :param imputeValue: String, float or int value to impute in place of mask or
        nulls.
        :param maskValue: String, float or int value which should be ignored when
        computing the imputation statistic and to be replaced by the imputation.
        :returns: None - class instantiated.
        """
        super().__init__()
        kwargs = self._input_kwargs
        self.setParams(**kwargs)

    @property
    def compatible_dtypes(self) -> Optional[List[DataType]]:
        """
        List of compatible data types for the layer.
        If the computation can be performed on any data type, return None.

        :returns: List of compatible data types for the layer.
        """
        return None

    def _transform(self, dataset: DataFrame) -> DataFrame:
        """
        Transforms the input data by imputing the specified
        statistic of the input column provided.
        Nulls and any values equalling the mask are imputed over.

        :param dataset: Pyspark dataframe to transform.
        :returns: Pyspark dataframe with the input columns
        imputed with the specified statistic, and names set to
        output column names provided
        """
        input_datatype = self.get_column_datatype(
            dataset=dataset, column_name=self.getInputCol()
        )
        output_col = single_input_single_output_scalar_transform(
            input_col=F.col(self.getInputCol()),
            input_col_datatype=input_datatype,
            func=lambda x: F.when(
                (x == F.lit(self.getMaskValue())) | (x.isNull()),
                F.lit(self.getImputeValue()),
            ).otherwise(x),
        )
        return dataset.withColumn(self.getOutputCol(), output_col)

    def get_tf_layer(self) -> tf.keras.layers.Layer:
        """
        Gets the tensorflow layer for the imputation transformer.

        :returns: Tensorflow keras layer with name equal to the layerName parameter
         that performs the imputation.
        """
        mask_value = self.getMaskValue()

        return ImputeLayer(
            name=self.getLayerName(),
            input_dtype=self.getInputTFDtype(),
            output_dtype=self.getOutputTFDtype(),
            impute_value=self.getImputeValue(),
            mask_value=mask_value,
        )
