# -------------------------------------------------------------------------
#  This file is part of the MindStudio project.
# Copyright (c) 2025 Huawei Technologies Co.,Ltd.
#
# MindStudio is licensed under Mulan PSL v2.
# You can use this software according to the terms and conditions of the Mulan PSL v2.
# You may obtain a copy of Mulan PSL v2 at:
#
#          http://license.coscl.org.cn/MulanPSL2
#
# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
# EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
# MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
# See the Mulan PSL v2 for more details.
# -------------------------------------------------------------------------

import os

import torch

from atk.common.log import Logger
from atk.common.utils import get_file_md5
from atk.configs.results_config import AccuracyConfig
from atk.configs.single_benchmark_config import (
    SingleBenchmarkCompareStandard,
    SingleBenchSummary
)
from atk.tasks.post_process import ACCURACY_REGISTRY
from atk.tasks.post_process.base_compare import BaseAccuracyCompare
from atk.tasks.post_process.utils import check_invalid_value
from atk.tasks.post_process.equal_compare import Compare as equal_compare

logging = Logger().get_logger()


@ACCURACY_REGISTRY.register("single_bm")
@ACCURACY_REGISTRY.register("default")
class SingleBenchmarkAccuracyCompare(BaseAccuracyCompare):
    def __init__(self, case_config, **kwargs):
        super(SingleBenchmarkAccuracyCompare, self).__init__(case_config, **kwargs)
        logging.debug(f"SingleBenchmark accuracy compare kwargs = {kwargs}", )
        self.benchmark_standard = SingleBenchmarkCompareStandard()
        self.benchmark_standard.update(**kwargs)

    @staticmethod
    def check_data_equality(local_output, remote_output):
        return torch.equal(local_output, remote_output)

    @staticmethod
    def compute_quantize_accuracy(local_output, remote_output, data_file):
        '''
        量化计算:int8输出,绝对误差小于等于1
        '''

        diff_value = torch.subtract(local_output.to(torch.int64), remote_output)
        diff_abs = torch.abs(diff_value)

        flat_diff_abs = diff_abs.view(-1)
        max_diff, max_diff_idx = torch.max(flat_diff_abs, dim=0)
        result = torch.all(diff_abs <= 1)

        acc_result = AccuracyConfig(
            result=result.item(),
            filename=data_file,
            max_diff=max_diff.item(),
            max_diff_idx=max_diff_idx.item(),
        )
        return acc_result

    def compare_file_md5(self, local_data_path, remote_data_path):
        acc_result = None
        local_digest = get_file_md5(local_data_path)
        remote_digest = self.remote_manager.get_file_md5(remote_data_path)
        filename = os.path.basename(local_data_path)
        info = f"The data comparison between {local_data_path} and {remote_data_path} by md5(hashlib)"
        if local_digest == remote_digest:
            logging.debug(info + "is passed.")
            acc_result = AccuracyConfig(result=True, filename=filename)
        else:
            logging.debug(info + "is failed.")
        return acc_result

    def check_output_size(self, local_output, remote_output, data_file):
        acc_result = None
        if local_output.numel() == 0 and remote_output.numel() == 0:
            info = "The npu_output is [], and it is same as bm_output, the result of data_compare is Pass"
            logging.debug(info)
            acc_result = AccuracyConfig(filename=data_file, result=True, error_info=info)
        if local_output.size() != remote_output.size():
            error_info = (f"the size of npu output[{local_output.size()}] and "
                          f"benchmark[{remote_output.size()}] is not equal.")
            logging.error(error_info)
            acc_result = AccuracyConfig(filename=data_file, result=False, error_info=error_info)
        return acc_result

    def compute_accuracy_result(self, local_output, remote_output, data_file):
        if torch.is_complex(local_output):
            real_ret = self.compute_mere_mare(local_output.real, remote_output.real, data_file)
            imag_ret = self.compute_mere_mare(local_output.imag, remote_output.imag, data_file)
            acc_ret = AccuracyConfig(
                result=real_ret.result and imag_ret.result,
                filename=data_file,
                error_info=f"real: MERE={real_ret.mean_rel_err}, MARE={real_ret.max_rel_err}\n"
                           f"imag: MERE={imag_ret.mean_rel_err}, MARE={imag_ret.max_rel_err}",
            )
        else:
            acc_ret = self.compute_mere_mare(local_output, remote_output, data_file)
        return acc_ret

    def compute_mere_mare(self, local_output, remote_output, data_file):
        dtype = local_output.dtype
        if local_output.dtype in [
            torch.float16,
            torch.bfloat16,
        ] and remote_output.dtype in [torch.float32]:
            local_output = local_output.to(torch.float32)
            if check_invalid_value(remote_output) or check_invalid_value(local_output):
                acc_result = AccuracyConfig(result=True, filename=data_file)
                return acc_result

        if check_invalid_value(remote_output):
            acc_result = AccuracyConfig(result=True, filename=data_file)
            return acc_result

        if check_invalid_value(local_output):
            error_info = f"The NPU result file {data_file} contains nan/inf value"
            acc_result = AccuracyConfig(result=False, filename=data_file, error_info=error_info)
            return acc_result

        if dtype in [torch.int8]:
            logging.info(f"output is {dtype}, use quantize standard.")
            acc_result = self.compute_quantize_accuracy(local_output, remote_output, data_file)
            return acc_result

        if dtype in [torch.uint8, torch.int16, torch.int32, torch.int64]:
            logging.info(f"output is {dtype}, use binary consistency standard.")
            instance_equal_compare = equal_compare(self.case_config)
            acc_result = instance_equal_compare.compute_accuracy_result(
                local_output, remote_output, data_file
            )
            return acc_result

        threshold = self.benchmark_standard.get_threshold(dtype)
        mare_ratio = self.benchmark_standard.mare_ratio

        if threshold is None:
            acc_result = AccuracyConfig(filename=data_file)
            if self.check_data_equality(local_output, remote_output):
                acc_result.update(result=True)
            else:
                error_info = f"Actual value is different from benchmark value with equal comparison in {data_file}"
                acc_result.update(result=False, error_info=error_info)
            return acc_result

        acc_result = AccuracyConfig(filename=data_file)

        self.compute_mare(local_output, remote_output, threshold, mare_ratio, acc_result)

        benchmark_summary = SingleBenchSummary(acc_result, threshold, mare_ratio)
        error_info = benchmark_summary.get_result_msg()
        acc_result.update(result=benchmark_summary.check_result, error_info=error_info)
        return acc_result

    def compute_mare(self, local_output, remote_output, threshold, mare_ratio, acc_result: AccuracyConfig):
        eps = 1e-7
        diff = torch.abs(local_output - remote_output)
        denom = torch.abs(remote_output) + eps
        rel_err = diff / denom

        mean_rel_err = torch.mean(rel_err).item()
        max_rel_err = torch.max(rel_err).item()

        acc_result.update(
            mean_rel_err=float(mean_rel_err),
            max_rel_err=float(max_rel_err),
        )