# coding:utf-8
# Copyright (c) 2021  PaddlePaddle Authors. All Rights Reserved.
#
# 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.
import json
import os
from typing import Any, Dict, List, Union

import numpy as np
import paddle
import paddle.nn.functional as F
from scipy.special import expit as np_sigmoid
from scipy.special import softmax as np_softmax

from ..data import DataCollatorWithPadding
from ..prompt import (
    AutoTemplate,
    PromptDataCollatorWithPadding,
    PromptModelForSequenceClassification,
    SoftVerbalizer,
)
from ..transformers import (
    AutoModelForMaskedLM,
    AutoModelForSequenceClassification,
    AutoTokenizer,
)
from ..utils.env import CONFIG_NAME, LEGACY_CONFIG_NAME
from ..utils.log import logger
from .task import Task
from .utils import static_mode_guard

usage = r"""
        from paddlenlp import Taskflow
        text_cls = Taskflow(
            "text_classification",
            mode="finetune",
            problem_type="multi_class",
            task_path=<local_saved_dynamic_model>,
            id2label={0: "negative", 1: "positive"}
            )
        text_cls('房间依然很整洁，相当不错')
        '''
        [
            {
                'text': '房间依然很整洁，相当不错',
                'predictions: [{
                    'label': 'positive',
                    'score': 0.80
                }]
            }
        ]
        '''
        text_cls = Taskflow(
            "text_classification",
            mode="prompt",
            problem_type="multi_label",
            is_static_model=True,
            task_path=<local_saved_static_model>,
            static_model_prefix=<static_model_prefix>,
            plm_model_path=<local_saved_plm_model>,
            id2label={ 0: "体育", 1: "经济", 2: "娱乐"}
            )
        text_cls(['这是一条体育娱乐新闻的例子',
                        '这是一条经济新闻'])
        '''
        [
            {
                'text': '这是一条体育娱乐新闻的例子',
                'predictions: [
                    {
                        'label': '体育',
                        'score': 0.80
                    },
                    {
                        'label': '娱乐',
                        'score': 0.90
                    }
                ]
            },
            {
                'text': '这是一条经济新闻',
                'predictions: [
                    {
                    'label': '经济',
                    'score': 0.80
                    }
                ]
            }
        ]
         """


def softmax(x, axis=None):
    x_max = np.amax(x, axis=axis, keepdims=True)
    exp_x_shifted = np.exp(x - x_max)
    return exp_x_shifted / np.sum(exp_x_shifted, axis=axis, keepdims=True)


class TextClassificationTask(Task):
    """
    The text classification model to classify text.
    NOTE: This task is different from all other tasks that it has no out-of-box zero-shot capabilities.
    Instead, it's used as a simple inference pipeline.

    Args:
        task (string): The name of task.
        model (string): Mode of the classification, Supports ["prompt", "finetune"].
        kwargs (dict, optional): Additional keyword arguments passed along to the specific task.
            task_path (string): The local file path to the model path or a pre-trained model.
            is_static_model (string): Whether the model in task path  is a static model.
            problem_type (str, optional): Select among ["multi_class", "multi_label"] based on the nature of your problem. Default to "multi_class".
            multilabel_threshold (float): The probability threshold used for the multi_label setup. Only effective if model = "multi_label". Defaults to 0.5.
            max_length (int): Maximum number of tokens for the model.
            precision (int): Select among ["fp32", "fp16"]. Default to "fp32".
            plm_model_name (str): Pretrained language model name for PromptModel.
            input_spec [list]: Specify the tensor information for each input parameter of the forward function.
            id2label(dict(int,string)): The dictionary to map the predictions from class ids to class names.
            batch_size(int): The sample number of a mini-batch.
    """

    def __init__(self, task: str, model: str = "finetune", **kwargs):
        super().__init__(task=task, model=model, **kwargs)
        self.problem_type = self.kwargs.get("problem_type", "multi_class")
        self.multilabel_threshold = self.kwargs.get("multilabel_threshold", 0.5)
        self._max_length = self.kwargs.get("max_length", 512)

        self._construct_tokenizer()
        if self.model == "prompt":
            self._initialize_prompt()
        self._check_predictor_type()
        self._get_inference_model()
        self._construct_id2label()

    def _initialize_prompt(self):
        if "plm_model_name" in self.kwargs:
            self._plm_model = AutoModelForMaskedLM.from_pretrained(self.kwargs["plm_model_name"])
        elif os.path.isdir(os.path.join(self._task_path, "plm")):
            self._plm_model = AutoModelForMaskedLM.from_pretrained(os.path.join(self._task_path, "plm"))
            logger.info(f"Load pretrained language model from {self._plm_model}")
        else:
            raise NotImplementedError(
                "Please specify the pretrained language model name （ex. plm_model_name='ernie-3.0-medium-zh'）."
            )
        self._template = AutoTemplate.load_from(self._task_path, self._tokenizer, self._max_length, self._plm_model)
        with open(os.path.join(self._task_path, "verbalizer_config.json"), "r", encoding="utf-8") as fp:
            self._label_words = json.load(fp)
        self._verbalizer = SoftVerbalizer(self._label_words, self._tokenizer, self._plm_model)

    def _construct_input_spec(self):
        """
        Construct the input spec for the convert dygraph model to static model.
        """
        if "input_spec" in self.kwargs:
            self._input_spec = self.kwargs["input_spec"]
        elif self.model == "finetune":
            if os.path.exists(os.path.join(self._task_path, LEGACY_CONFIG_NAME)):
                with open(os.path.join(self._task_path, LEGACY_CONFIG_NAME)) as fb:
                    init_class = json.load(fb)["init_class"]
            elif os.path.exists(os.path.join(self._task_path, CONFIG_NAME)):
                with open(os.path.join(self._task_path, CONFIG_NAME)) as fb:
                    init_class = json.load(fb)["architectures"].pop()
            else:
                raise IOError(
                    f"Model configuration file doesn't exist.[task_path] should include {LEGACY_CONFIG_NAME} or {CONFIG_NAME}"
                )

            if init_class in ["ErnieMForSequenceClassification"]:
                self._input_spec = [paddle.static.InputSpec(shape=[None, None], dtype="int64", name="input_ids")]
            else:
                self._input_spec = [
                    paddle.static.InputSpec(shape=[None, None], dtype="int64", name="input_ids"),
                    paddle.static.InputSpec(shape=[None, None], dtype="int64", name="token_type_ids"),
                ]
        elif self.model == "prompt":
            self._input_spec = self._model.get_input_spec()
        else:
            raise NotImplementedError(
                f"'{self.model}' is not a supported model_type. Please select among ['finetune', 'prompt']"
            )

    def _construct_model(self, model: str):
        """
        Construct the inference model for the predictor.
        """
        if model == "finetune":
            model_instance = AutoModelForSequenceClassification.from_pretrained(self._task_path)
        elif model == "prompt":
            model_instance = PromptModelForSequenceClassification(self._plm_model, self._template, self._verbalizer)
            state_dict = paddle.load(os.path.join(self._task_path, "model_state.pdparams"), return_numpy=True)
            model_instance.set_state_dict(state_dict)
            # release memory
            del state_dict
        else:
            raise NotImplementedError(
                f"'{model}' is not a supported model_type. Please select among ['finetune', 'prompt']"
            )

        # Load the model parameter for the predict
        model_instance.eval()
        self._model = model_instance

    def _construct_tokenizer(self):
        """
        Construct the tokenizer for the predictor.
        """
        self._tokenizer = AutoTokenizer.from_pretrained(self._task_path)

    def _construct_id2label(self):
        if "id2label" in self.kwargs:
            id2label = self.kwargs["id2label"]
        elif os.path.exists(os.path.join(self._task_path, "id2label.json")):
            id2label_path = os.path.join(self._task_path, "id2label.json")
            with open(id2label_path) as fb:
                id2label = json.load(fb)
            logger.info(f"Load id2label from {id2label_path}.")
        elif self.model == "prompt" and os.path.exists(os.path.join(self._task_path, "verbalizer_config.json")):
            label_list = sorted(list(self._verbalizer.label_words.keys()))
            id2label = {}
            for i, l in enumerate(label_list):
                id2label[i] = l
            logger.info("Load id2label from verbalizer.")
        elif self.model == "finetune" and os.path.exists(os.path.join(self._task_path, CONFIG_NAME)):
            config_path = os.path.join(self._task_path, CONFIG_NAME)
            with open(config_path) as fb:
                config = json.load(fb)
                if "id2label" in config:
                    id2label = config["id2label"]
                    logger.info(f"Load id2label from {config_path}.")
                else:
                    id2label = None
        else:
            id2label = None

        if id2label is None:
            self.id2label = id2label
        else:
            self.id2label = {}
            for i in id2label:
                self.id2label[int(i)] = id2label[i]

    def _preprocess(self, inputs: Union[str, List[str]]) -> Dict[str, Any]:
        """
        Transform the raw text to the model inputs, two steps involved:
           1) Transform the raw text to token ids.
           2) Generate the other model inputs from the raw text and token ids.
        """
        inputs = self._check_input_text(inputs)
        # Get the config from the kwargs
        batch_size = self.kwargs["batch_size"] if "batch_size" in self.kwargs else 1

        if self.model == "finetune":
            collator = DataCollatorWithPadding(self._tokenizer, return_tensors="np")
            tokenized_inputs = [self._tokenizer(i, max_length=self._max_length, truncation=True) for i in inputs]
            batches = [tokenized_inputs[idx : idx + batch_size] for idx in range(0, len(tokenized_inputs), batch_size)]
        elif self.model == "prompt":
            collator = PromptDataCollatorWithPadding(
                self._tokenizer, padding=True, return_tensors="np", return_attention_mask=True
            )
            part_text = "text"
            for part in self._template.prompt:
                if "text" in part:
                    part_text = part["text"]
            template_inputs = [self._template({part_text: x}) for x in inputs]
            batches = [template_inputs[idx : idx + batch_size] for idx in range(0, len(template_inputs), batch_size)]
        else:
            raise NotImplementedError(
                f"'{self.model}' is not a supported model_type. Please select among ['finetune', 'prompt']"
            )
        outputs = {}
        outputs["text"] = inputs
        outputs["batches"] = [collator(batch) for batch in batches]

        return outputs

    def _run_model(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
        """
        Run the task model from the outputs of the `_tokenize` function.
        """
        # TODO: support hierarchical classification
        outputs = {}
        outputs["text"] = inputs["text"]
        outputs["batch_logits"] = []
        dtype_dict = {
            "input_ids": "int64",
            "token_type_ids": "int64",
            "position_ids": "int64",
            "attention_mask": "float32",
            "masked_positions": "int64",
            "soft_token_ids": "int64",
            "encoder_ids": "int64",
        }
        with static_mode_guard():
            for batch in inputs["batches"]:
                if "attention_mask" in batch:
                    input_name = "attention_mask"
                    if batch[input_name].ndim == 2:
                        batch[input_name] = (1 - batch[input_name][:, np.newaxis, np.newaxis, :]) * -1e4
                    elif batch[input_name].ndim != 4:
                        raise ValueError(
                            "Expect attention mask with ndim=2 or 4, but get ndim={}".format(batch[input_name].ndim)
                        )
                if self._predictor_type == "paddle-inference":
                    for i, input_name in enumerate(self.predictor.get_input_names()):
                        self.input_handles[i].copy_from_cpu(batch[input_name].astype(dtype_dict[input_name]))
                    self.predictor.run()
                    logits = self.output_handle[0].copy_to_cpu().tolist()
                else:
                    input_dict = {}
                    for input_name in self.input_handler:
                        input_dict[input_name] = batch[input_name].astype(dtype_dict[input_name])
                    logits = self.predictor.run(None, input_dict)[0].tolist()
                outputs["batch_logits"].append(logits)
        return outputs

    def _postprocess(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
        """
        This function converts the model logits output to class score and predictions
        """
        # TODO: support hierarchical classification
        postprocessed_outputs = []
        for logits in inputs["batch_logits"]:
            if self.problem_type == "multi_class":
                if isinstance(logits, paddle.Tensor):  # dygraph
                    scores = F.softmax(logits, axis=-1).numpy()
                    labels = paddle.argmax(logits, axis=-1).numpy()
                else:  # static graph
                    scores = np_softmax(logits, axis=-1)
                    labels = np.argmax(logits, axis=-1)
                for score, label in zip(scores, labels):
                    postprocessed_output = {}
                    if self.id2label is None:
                        postprocessed_output["predictions"] = [{"label": label, "score": score[label]}]
                    else:
                        postprocessed_output["predictions"] = [{"label": self.id2label[label], "score": score[label]}]
                    postprocessed_outputs.append(postprocessed_output)
            elif self.problem_type == "multi_label":  # multi_label
                if isinstance(logits, paddle.Tensor):  # dygraph
                    scores = F.sigmoid(logits).numpy()
                else:  # static graph
                    scores = np_sigmoid(logits)
                for score in scores:
                    postprocessed_output = {}
                    postprocessed_output["predictions"] = []
                    for i, class_score in enumerate(score):
                        if class_score > self.multilabel_threshold:
                            if self.id2label is None:
                                postprocessed_output["predictions"].append({"label": i, "score": class_score})
                            else:
                                postprocessed_output["predictions"].append(
                                    {"label": self.id2label[i], "score": class_score}
                                )
                    postprocessed_outputs.append(postprocessed_output)
            else:
                raise NotImplementedError(
                    f"'{self.problem_type}' is not a supported problem type. Please select among ['multi_class', 'multi_label']"
                )
        for i, postprocessed_output in enumerate(postprocessed_outputs):
            postprocessed_output["text"] = inputs["text"][i]
        return postprocessed_outputs
