# Copyright (c) 2022 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.
from __future__ import annotations

import io
import os
import pickle
from functools import lru_cache
from typing import Union
from zipfile import ZipFile

import numpy as np
import paddle
from _io import BufferedReader
from safetensors import deserialize

from paddlenlp.utils.env import PYTORCH_WEIGHTS_NAME, SAFE_WEIGHTS_NAME

MZ_ZIP_LOCAL_DIR_HEADER_SIZE = 30


_TYPES = {
    "F64": np.float64,
    "F32": np.float32,
    "F16": np.float16,
    "I64": np.int64,
    "U64": np.uint64,
    "I32": np.int32,
    "U32": np.uint32,
    "I16": np.int16,
    "U16": np.uint16,
    "BF16": np.uint16,
    "I8": np.int8,
    "U8": np.uint8,
    "BOOL": bool,
}


class SerializationError(Exception):
    """Exception for serialization"""

    pass


def seek_by_string(file_handler: BufferedReader, string: str, file_size: int) -> int:
    """seek the index of file-handler with target words
    Args:
        file_handler (BufferedReader): file handler
        string (str): the specific string in the file
        file_size (int): size of file
    Returns:
        int: end index of target string
    """
    word_index = 0
    word_bytes = string.encode("latin")
    empty_byte = "".encode("latin")

    while word_index < len(string) and file_handler.tell() < file_size:
        content = file_handler.read(1)
        if content == empty_byte:
            break

        if word_bytes[word_index] == content[0]:
            word_index += 1
        else:
            word_index = 0

    if file_handler.tell() >= file_size - 1:
        raise SerializationError(f"can't find the find the target string<{string}> in the file")
    return file_handler.tell()


def read_prefix_key(path):
    file_size = os.stat(path).st_size
    with open(path, "rb") as file_handler:
        end_index = seek_by_string(file_handler, "data.pkl", file_size)
        file_handler.seek(MZ_ZIP_LOCAL_DIR_HEADER_SIZE)
        prefix_key = file_handler.read(end_index - MZ_ZIP_LOCAL_DIR_HEADER_SIZE - len("/data.pkl"))
    return prefix_key.decode("latin")


def _maybe_decode_ascii(bytes_str: Union[bytes, str]) -> str:
    if isinstance(bytes_str, bytes):
        return bytes_str.decode("ascii")
    return bytes_str


@lru_cache(maxsize=None)
def _storage_type_to_dtype_to_map():
    """convert storage type to numpy dtype"""
    return {
        "DoubleStorage": np.double,
        "FloatStorage": np.float32,
        "HalfStorage": np.half,
        "LongStorage": np.int64,
        "IntStorage": np.int32,
        "ShortStorage": np.int16,
        "CharStorage": np.int8,
        "ByteStorage": np.uint8,
        "BoolStorage": np.bool_,
        "ComplexDoubleStorage": np.cdouble,
        "ComplexFloatStorage": np.cfloat,
        "BFloat16Storage": np.uint16,  # support bf16
    }


class StorageType:
    """Temp Class for Storage Type"""

    def __init__(self, name):
        self.dtype = _storage_type_to_dtype_to_map()[name]

    def __str__(self):
        return f"StorageType(dtype={self.dtype})"


def _element_size(dtype: str) -> int:
    """
    Returns the element size for a dtype, in bytes
    """
    if dtype in [np.float16, np.float32, np.float64]:
        return np.finfo(dtype).bits >> 3
    elif dtype == np.bool_:
        return 1
    else:
        return np.iinfo(dtype).bits >> 3


class UnpicklerWrapperStage(pickle.Unpickler):
    def find_class(self, mod_name, name):
        if type(name) is str and "Storage" in name:
            try:
                return StorageType(name)
            except KeyError:
                pass

        if mod_name == "torch._utils":
            # rebuild torch.nn.Papameter
            if name == "_rebuild_parameter":
                return _rebuild_parameter
            # rebuild torch.nn.Papameter with state
            if name == "_rebuild_parameter_with_state":
                return _rebuild_parameter_with_state
            # rebuild torch.Tensor
            return _rebuild_tensor_stage

        # pytorch_lightning tensor builder
        if "pytorch_lightning" in mod_name:
            return dumpy
        return super().find_class(mod_name, name)


class SafeUnpickler(pickle.Unpickler):
    """
    A safe unpickler that only allows loading of built-in basic data types.
    """

    def find_class(self, module, name):
        """
        Overrides the find_class method to only allow loading of built-in basic data types.

        :param module: The module name.
        :param name: The class name.
        :return: The class if allowed, otherwise raises UnpicklingError.
        """
        if module == "builtins" and name in {"int", "float", "str", "tuple", "list", "dict", "set"}:
            return super().find_class(module, name)
        raise pickle.UnpicklingError(f"Unsafe object loading is prohibited: {module}.{name}")


def _rebuild_tensor_stage(storage, storage_offset, size, stride, requires_grad, backward_hooks):
    # if a tensor has shape [M, N] and stride is [1, N], it's column-wise / fortran-style
    # if a tensor has shape [M, N] and stride is [M, 1], it's row-wise / C-style
    # defaults to C-style
    if stride is not None and len(stride) > 1 and stride[0] == 1 and stride[1] > 1:
        order = "F"
    else:
        order = "C"

    # fix bug when load https://dl.fbaipublicfiles.com/segment_anything/sam_vit_h_4b8939.pth
    numel = int(np.prod(size))
    return storage[storage_offset : storage_offset + numel].reshape(size, order=order)


def _rebuild_parameter(data, requires_grad, backward_hooks):
    return data


def _rebuild_parameter_with_state(data, requires_grad, backward_hooks, state):
    return data


def dumpy(*args, **kwarsg):
    return None


def load_torch(path: str, **pickle_load_args):
    from paddlenlp.transformers.utils import device_guard

    if path.endswith(PYTORCH_WEIGHTS_NAME) or os.path.split(path)[-1].startswith("pytorch_model-"):
        import torch

        state_dict = torch.load(path, map_location="cpu", weights_only=False)

        for key in list(state_dict.keys()):
            if isinstance(state_dict[key], torch.Tensor):
                t = state_dict.pop(key)
                capsule = torch.utils.dlpack.to_dlpack(t)
                t = paddle.utils.dlpack.from_dlpack(capsule)
                state_dict[key] = t
        return state_dict
    elif path.endswith(SAFE_WEIGHTS_NAME) or os.path.split(path)[-1].startswith("model-"):
        # torch safetensors -> numpy -> paddle.Tensor
        with open(path, "rb") as f:
            data = f.read()

        flat = deserialize(data)
        state_dict = {}
        for k, v in flat:
            dtype = _TYPES[v["dtype"]]
            with device_guard("cpu"):
                if v["dtype"] == "BF16":
                    arr = paddle.Tensor(
                        np.frombuffer(v["data"], dtype=dtype).reshape(v["shape"]), dtype="bfloat16", zero_copy=True
                    )
                else:
                    arr = paddle.Tensor(np.frombuffer(v["data"], dtype=dtype).reshape(v["shape"]), zero_copy=True)

            state_dict[k] = arr

    return state_dict


def load_torch_inner(path: str, **pickle_load_args):
    """
    load torch weight file with the following steps:
    1. load the structure of pytorch weight file
    2. read the tensor data and re-construct the state-dict
    Args:
        path: the path of pytorch weight file
        **pickle_load_args: args of pickle module
    Returns:
    """

    if path.endswith(PYTORCH_WEIGHTS_NAME) or os.path.split(path)[-1].startswith("pytorch_model-"):
        pickle_load_args.update({"encoding": "utf-8"})

        prefix_key = read_prefix_key(path)

        torch_zip = ZipFile(path, "r")
        loaded_storages = {}

        def load_tensor(dtype, numel, key, location):
            name = f"{prefix_key}/data/{key}"
            typed_storage = np.frombuffer(torch_zip.open(name).read()[:numel], dtype=dtype)
            return typed_storage

        def persistent_load(saved_id):
            assert isinstance(saved_id, tuple)
            typename = _maybe_decode_ascii(saved_id[0])
            data = saved_id[1:]

            assert (
                typename == "storage"
            ), f"Unknown typename for persistent_load, expected 'storage' but got '{typename}'"
            storage_type, key, location, numel = data
            dtype = storage_type.dtype

            if key in loaded_storages:
                typed_storage = loaded_storages[key]
            else:
                nbytes = numel * _element_size(dtype)
                typed_storage = load_tensor(dtype, nbytes, key, _maybe_decode_ascii(location))
                loaded_storages[key] = typed_storage

            return typed_storage

        data_iostream = torch_zip.open(f"{prefix_key}/data.pkl").read()
        unpickler_stage = UnpicklerWrapperStage(io.BytesIO(data_iostream), **pickle_load_args)
        unpickler_stage.persistent_load = persistent_load
        state_dict = unpickler_stage.load()
        torch_zip.close()
    elif path.endswith(SAFE_WEIGHTS_NAME) or os.path.split(path)[-1].startswith("model-"):
        # torch safetensors -> numpy -> paddle.Tensor
        with open(path, "rb") as f:
            data = f.read()

        flat = deserialize(data)
        state_dict = {}
        for k, v in flat:
            dtype = _TYPES[v["dtype"]]
            if v["dtype"] == "BF16":
                arr = paddle.to_tensor(np.frombuffer(v["data"], dtype=dtype).reshape(v["shape"]), dtype="bfloat16")
            else:
                arr = paddle.to_tensor(np.frombuffer(v["data"], dtype=dtype).reshape(v["shape"]))
            state_dict[k] = arr

    return state_dict
