# Copyright 2025 NVIDIA CORPORATION & AFFILIATES
#
# 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.
#
# SPDX-License-Identifier: Apache-2.0
# Modified from LLaDA repos: https://github.com/ML-GSAI/LLaDA

'''
This file is inspired by the code from https://github.com/ML-GSAI/SMDM
'''
import accelerate
import torch
import re
from pathlib import Path
import random
import numpy as np
import torch.nn.functional as F
from datasets import Dataset
from lm_eval.__main__ import cli_evaluate
from lm_eval.api.instance import Instance
from lm_eval.api.model import LM
from lm_eval.api.registry import register_model
from tqdm import tqdm
import os
import sys
from transformers import AutoTokenizer, AutoModelForCausalLM, AutoConfig
import json
import time
import types

# Add chengyue_script directory to path for modeling_dfm import
_script_dir = os.path.dirname(os.path.abspath(__file__))
_chengyue_dvlm_script_dir = os.path.dirname(os.path.dirname(_script_dir))  # chengyue_script
_models_dir = os.path.join(_chengyue_dvlm_script_dir, "models")
if _models_dir not in sys.path:
    sys.path.insert(0, _models_dir)

from generation_functions import Fast_dLLM_QwenForCausalLM

def set_seed(seed):
    torch.manual_seed(seed)
    random.seed(seed)
    np.random.seed(seed)

    torch.backends.cudnn.deterministic = True
    torch.backends.cudnn.benchmark = False


@register_model("dfm_qwen")
class DFMQwenEvalHarness(LM):
    def __init__(
        self,
        model_path='',
        device="cuda",
        is_instruct=True,
        show_speed=False,
        max_new_tokens=2048,
        temperature=0.0,
        do_sample=False,
        batch_size=32,
        mask_id=151665,
        bd_size=32,
        mc_num=1,
        cfg=0.0,
        threshold=0.9,
        force_left_to_right=False,
        speculative_block_causal=False,
        debug=False,
        sub_block_size=None,
        **kwargs,
    ):

        super().__init__()

        accelerator = accelerate.Accelerator()
        if accelerator.num_processes > 1:
            self.accelerator = accelerator
        else:
            self.accelerator = None
        
        model_kwargs = {}
        if self.accelerator is not None:
            model_kwargs.update({'device_map': {'': f'{self.accelerator.device}'}})
        
        # 使用AutoModelForCausalLM替代AutoModel
        from lmflow.models.qwen2_5_vl_mdm import Fast_dLLM_Qwen2_5_VLForConditionalGeneration
        self.model = Fast_dLLM_Qwen2_5_VLForConditionalGeneration.from_pretrained(
            model_path,
            torch_dtype=torch.float32,
            trust_remote_code=True,
            # device_map="cuda:0",
        )
        # self.model = AutoModelForCausalLM.from_pretrained(
        #     model_path, 
        #     trust_remote_code=True, 
        #     torch_dtype=torch.bfloat16, 
        #     **model_kwargs
        # )
        self.model.eval()

        # 绑定mdm_sample方法，匹配chatbot逻辑
        self.model.mdm_sample = types.MethodType(Fast_dLLM_QwenForCausalLM.block_causal_sample_cache, self.model)
        self.model.speculative_block_causal_sample_cache = types.MethodType(Fast_dLLM_QwenForCausalLM.speculative_block_causal_sample_cache, self.model)
        self.device = torch.device(device)
        if self.accelerator is not None:
            self.model = self.accelerator.prepare(self.model)
            self.device = torch.device(f'{self.accelerator.device}')
            self._rank = self.accelerator.local_process_index
            self._world_size = self.accelerator.num_processes
        else: 
            self.model = self.model.to(device)
            self._rank = 0
            self._world_size = 1

        self.tokenizer = AutoTokenizer.from_pretrained(model_path, trust_remote_code=True)

        
        # 添加缺失的属性
        self.is_instruct = is_instruct
        self.show_speed = show_speed
        self.max_new_tokens = max_new_tokens
        self.temperature = temperature
        self.do_sample = do_sample
        self.batch_size = int(batch_size)
        self.mask_id = mask_id
        self.model_path = model_path
        self.threshold = threshold
        self.mc_num = mc_num
        self.cfg = cfg
        self.bd_size = bd_size
        self.force_left_to_right = force_left_to_right
        self.speculative_block_causal = speculative_block_causal
        self.debug = debug
        self.sub_block_size = sub_block_size
    @property
    def rank(self):
        return self._rank
    
    @property
    def world_size(self):
        return self._world_size

    @property
    def tokenizer_name(self):
        return self.model_path
    
    def apply_chat_template(self, chat_history, add_generation_prompt=True):
        return self.tokenizer.apply_chat_template(chat_history, add_generation_prompt=add_generation_prompt, tokenize=False)
    
    def loglikelihood_rolling(self, requests):
        raise NotImplementedError
    
    def _encode_pair(self, context, continuation):
        whole_enc = self.tokenizer(context + continuation)["input_ids"]
        context_enc = self.tokenizer(context)["input_ids"]

        context_enc_len = len(context_enc)
        continuation_enc = whole_enc[context_enc_len:]

        return context_enc, continuation_enc


    def _forward_process(self, batch, prompt_index):
        b, l = batch.shape

        target_len = (l - prompt_index.sum()).item()
        # k = torch.randint(1, target_len + 1, (), device=batch.device)

        # x = torch.round(torch.linspace(float(k), k + (b - 1) * (target_len / b), steps=b, device=batch.device)).long()
        # x = ((x - 1) % target_len) + 1
        # assert x.min() >= 1 and x.max() <= target_len

        # indices = torch.arange(target_len, device=batch.device).repeat(b, 1)
        # is_mask = indices < x.unsqueeze(1)

        # for i in range(b):
        #     is_mask[i] = is_mask[i][torch.randperm(target_len)]
        # is_mask = torch.cat((torch.zeros(b, prompt_index.sum(), dtype=torch.bool, device=batch.device), is_mask), dim=1)

        is_mask = torch.zeros_like(batch, dtype=torch.bool, device=batch.device)
        is_mask[:, prompt_index.sum()] = True


        noisy_batch = torch.where(is_mask, self.mask_id, batch)
        noisy_batch = torch.cat([noisy_batch, torch.full((b, self.bd_size-noisy_batch.shape[1]%self.bd_size), self.mask_id, dtype=torch.long, device=batch.device)], dim=1)
        if noisy_batch.shape[1] > l:
            noisy_batch[:, l] = self.tokenizer.eos_token_id
        # if noisy_batch.shape[1] > l+1:
        #     noisy_batch[:, l+1] = 198
        # l = noisy_batch.shape[1]
        # import pdb; pdb.set_trace()
        # return noisy_batch, (x / target_len).unsqueeze(1).repeat(1, l)
        return noisy_batch

    @torch.no_grad()
    def get_logits(self, batch, prompt_index):
        if self.cfg > 0.:
            assert len(prompt_index) == batch.shape[1]
            prompt_index = prompt_index.unsqueeze(0).repeat(batch.shape[0], 1)
            un_batch = batch.clone()
            un_batch[prompt_index] = self.mask_id
            batch = torch.cat([batch, un_batch])

        logits = self.model(batch).logits

        if self.cfg > 0.:
            logits, un_logits = torch.chunk(logits, 2, dim=0)
            logits = un_logits + (self.cfg + 1) * (logits - un_logits)
        if self.token_shift:
            logits = torch.cat([logits[:, :1, :], logits[:, :-1, :]], dim=1)
        return logits[:, :batch.shape[1]]

    @torch.no_grad()
    def get_loglikelihood(self, prefix, target):
        seq = torch.concatenate([prefix, target])[None, :]
        seq = seq.repeat((self.batch_size, 1)).to(self.device)

        prompt_index = torch.arange(seq.shape[1], device=self.device) < len(prefix)

        loss_acc = []
        for _ in range(self.mc_num // self.batch_size):
            # perturbed_seq, p_mask = self._forward_process(seq, prompt_index)
            perturbed_seq = self._forward_process(seq, prompt_index)

            mask_indices = perturbed_seq == self.mask_id

            logits = self.get_logits(perturbed_seq, prompt_index)
            seq = torch.cat([seq, torch.full((seq.shape[0], self.bd_size-seq.shape[1]%self.bd_size), -100, dtype=torch.long, device=self.device)], dim=1)
            # loss = F.cross_entropy(logits[mask_indices], seq[mask_indices], reduction='none') / p_mask[mask_indices]
            loss = F.cross_entropy(logits[mask_indices], seq[mask_indices], reduction='none')
            loss = loss.sum() / self.batch_size
            loss_acc.append(loss.item())

        return - sum(loss_acc) / len(loss_acc)


    def loglikelihood(self, requests):
        def _tokenize(e):
            prefix, target = self._encode_pair(e["prefix"], e["target"])
            return {
                "prefix_text": e["prefix"],
                "target_text": e["target"],
                "prefix": prefix,
                "target": target,
            }

        ds = []
        ds = [{"prefix": req.args[0], "target": req.args[1]} for req in requests]
        ds = Dataset.from_list(ds)
        ds = ds.map(_tokenize)
        ds = ds.with_format("torch")
        prompt_len = [len(x["prefix"]) + len(x["target"]) for x in ds]

        assert max(prompt_len) <= 4096

        out = []
        with torch.no_grad():
            for elem in tqdm(ds, desc="Computing likelihood..."):
                prefix = elem["prefix"]
                target = elem["target"]
                # assert len(target) == 2, f"target {elem['target_text']} length is {len(target)}"

                ll = self.get_loglikelihood(prefix, target)
                out.append((ll, 0.0))
        torch.cuda.empty_cache()
        return out

    def generate_until(self, requests):
        output = []
        num_tokens = 0

        
        start_time = time.time()
        
        for i, req in enumerate(tqdm(requests, desc="Generating...")):
                
            question = req.args[0]
            model_inputs = self.tokenizer([question], return_tensors="pt").to(self.device)
            
            # 使用mdm_sample方法，匹配chatbot逻辑
            with torch.no_grad():
                if self.accelerator is not None:
                    if self.speculative_block_causal:
                        generated_ids = self.accelerator.unwrap_model(self.model).speculative_block_causal_sample_cache(
                            model_inputs["input_ids"],
                            tokenizer=self.tokenizer,
                            block_size=self.bd_size,
                            max_tokens=self.max_new_tokens,
                            mask_id=self.tokenizer.encode("|<MASK>|")[0],
                            debug=self.debug,
                            sub_block_size=self.sub_block_size,
                        )
                    else:
                        generated_ids = self.accelerator.unwrap_model(self.model).mdm_sample(
                        model_inputs["input_ids"],
                        tokenizer=self.tokenizer,
                        block_size=self.bd_size,
                        max_tokens=self.max_new_tokens,
                        mask_id=self.tokenizer.encode("|<MASK>|")[0],
                        threshold=self.threshold,
                        force_left_to_right=self.force_left_to_right,
                        debug=self.debug,
                        output_file="decoded_output_eval.txt",
                        sub_block_size=self.sub_block_size,
                    )
                else:
                    if self.speculative_block_causal:
                        generated_ids = self.model.speculative_block_causal_sample_cache(
                            model_inputs["input_ids"],
                            tokenizer=self.tokenizer,
                            block_size=self.bd_size,
                            max_tokens=self.max_new_tokens,
                            mask_id=self.tokenizer.encode("|<MASK>|")[0],
                            debug=self.debug,
                            sub_block_size=self.sub_block_size,
                        )
                    else:
                        generated_ids = self.model.mdm_sample(
                            model_inputs["input_ids"],
                            tokenizer=self.tokenizer,
                            block_size=self.bd_size,
                            max_tokens=self.max_new_tokens,
                            mask_id=self.tokenizer.encode("|<MASK>|")[0],
                            threshold=self.threshold,
                            force_left_to_right=self.force_left_to_right,
                            debug=self.debug,
                            output_file="decoded_output_eval.txt",
                            sub_block_size=self.sub_block_size,
                        )
            
            # 提取新生成的token，匹配chatbot的解码逻辑
            generated_answer = self.tokenizer.decode(
                generated_ids[0][model_inputs["input_ids"].shape[1]:], 
                skip_special_tokens=True
            )
            
            # 统计token数量
            if self.show_speed:
                num_tokens += len(self.tokenizer.encode(generated_answer, add_special_tokens=False))
            
            output.append(generated_answer)

            print('=' * 20)
            print('question: ', question)
            print('answer: ', generated_answer)
            print('=' * 20, end='\n\n')
            
        end_time = time.time()
        if self.show_speed:
            print(f"Total number of tokens generated: {num_tokens}")
            print(f"Total time taken: {end_time - start_time} seconds")
            print(f"Tokens per second: {num_tokens / (end_time - start_time)}")
            
        return output
if __name__ == "__main__":
    cli_evaluate()
    
