# Copyright (c) ModelScope Contributors. All rights reserved.
import itertools
import torch
from functools import partial
from typing import Any, Dict, List, Literal, Optional

from swift.utils import get_env_args, to_float_dtype
from ..base import Template
from ..constant import MLLMTemplateType
from ..register import TemplateMeta, register_template
from ..template_inputs import StdTemplateInputs
from ..utils import Context, findall
from ..vision_utils import load_batch, load_file
from .qwen import QwenTemplateMeta


class GOTImageEvalProcessor:

    def __init__(self, image_size=384, mean=None, std=None):
        from torchvision import transforms
        from torchvision.transforms.functional import InterpolationMode
        if mean is None:
            mean = (0.48145466, 0.4578275, 0.40821073)
        if std is None:
            std = (0.26862954, 0.26130258, 0.27577711)

        self.normalize = transforms.Normalize(mean, std)

        self.transform = transforms.Compose([
            transforms.Resize((image_size, image_size), interpolation=InterpolationMode.BICUBIC),
            transforms.ToTensor(),
            self.normalize,
        ])

    def __call__(self, item):
        return self.transform(item)


class GOT_OCR2Template(Template):
    placeholder_tokens = ['<imgpad>']

    def replace_tag(self, media_type: Literal['image', 'video', 'audio'], index: int,
                    inputs: StdTemplateInputs) -> List[Context]:
        # 'OCR: '
        # 'OCR with format: '
        assert media_type == 'image'
        return ['<img>' + '<imgpad>' * 256 + '</img>\n']

    def _encode(self, inputs: StdTemplateInputs) -> Dict[str, Any]:
        encoded = super()._encode(inputs)
        images = inputs.images
        image_processor_high = GOTImageEvalProcessor(image_size=1024)
        for i, image in enumerate(images):
            images[i] = image_processor_high(image)[None].to(self.model_info.torch_dtype)
        if images:
            encoded['images'] = images
        return encoded

    def _data_collator(self, batch: List[Dict[str, Any]], *, padding_to: Optional[int] = None) -> Dict[str, Any]:
        res = super()._data_collator(batch, padding_to=padding_to)
        images = self.gather_list(batch, 'images')
        if images:
            res['images'] = images
        return res


register_template(
    QwenTemplateMeta(
        MLLMTemplateType.got_ocr2,
        default_system='        You should follow the instructions carefully and explain your answers in detail.',
        template_cls=GOT_OCR2Template,
        agent_template=None,
    ))


class GOT_OCR2HfTemplate(Template):
    placeholder_tokens = ['<imgpad>']

    def replace_tag(self, media_type: Literal['image', 'video', 'audio'], index: int,
                    inputs: StdTemplateInputs) -> List[Context]:
        # 'OCR: '
        # 'OCR with format: '
        assert media_type == 'image'
        return ['<img>' + '<imgpad>' * 256 + '</img>\n']

    def _encode(self, inputs: StdTemplateInputs) -> Dict[str, Any]:  # 暂时照抄上面
        encoded = super()._encode(inputs)
        images = inputs.images
        if images:
            encoded['images'] = images
        return encoded

    def _data_collator(self, batch: List[Dict[str, Any]], *, padding_to: Optional[int] = None) -> Dict[str, Any]:
        res = super()._data_collator(batch, padding_to=padding_to)
        images = self.gather_list(batch, 'images')
        _inputs = self.processor(images, return_tensors='pt')
        _inputs.pop('input_ids')  # this does not contain the response, so cannot be used when training
        _inputs.pop('attention_mask')  # this does not contain the response, so cannot be used when training

        res.update(_inputs.data)
        return res


register_template(
    QwenTemplateMeta(
        MLLMTemplateType.got_ocr2_hf,
        default_system='        You should follow the instructions carefully and explain your answers in detail.',
        template_cls=GOT_OCR2HfTemplate,
        agent_template=None,
    ))


class StepAudioTemplate(Template):
    use_model = True

    def replace_tag(self, media_type: Literal['image', 'video', 'audio'], index: int,
                    inputs: StdTemplateInputs) -> List[Context]:
        assert media_type == 'audio', f'media_type: {media_type}'
        from utils import load_audio
        audio_wav, sr = load_audio(load_file(inputs.audios[index]))
        audio_tokens = self.model.encoder(audio_wav, sr)
        return audio_tokens


class StepAudio2MiniTemplate(Template):
    use_model = True

    def load_audio(self, file_path, target_rate=16000, max_length=None):
        '''
        Open an audio file and read as mono waveform, resampling as necessary
        If max_length is provided, truncate the audio to that length
        '''
        import torchaudio
        waveform, sample_rate = torchaudio.load(file_path)
        if sample_rate != target_rate:
            waveform = torchaudio.transforms.Resample(orig_freq=sample_rate, new_freq=target_rate)(waveform)
        audio = waveform[0]  # get the first channel

        # Truncate audio if it exceeds max_length
        if max_length is not None and audio.shape[0] > max_length:
            audio = audio[:max_length]

        return audio

    def _mel_filters(self, n_mels: int) -> 'torch.Tensor':
        '''Load the mel filterbank matrix for projecting STFT into a Mel spectrogram.'''
        import librosa
        import torch
        assert n_mels in {80, 128}, f'Unsupported n_mels: {n_mels}'
        if n_mels == 128:
            return torch.from_numpy(librosa.filters.mel(sr=16000, n_fft=400, n_mels=128))
        else:
            return torch.from_numpy(librosa.filters.mel(sr=16000, n_fft=400, n_mels=80))

    def log_mel_spectrogram(self, audio, n_mels=128, padding=479):
        '''
        Compute the log-Mel spectrogram with specific padding for StepAudio
        '''
        import torch
        import torch.nn.functional as F
        if isinstance(audio, str):
            audio = self.load_audio(audio)
        elif not torch.is_tensor(audio):
            audio = torch.from_numpy(audio)
        if padding > 0:
            audio = F.pad(audio, (0, padding))
        window = torch.hann_window(400).to(audio.device)
        stft = torch.stft(audio, 400, 160, window=window, return_complex=True)
        magnitudes = stft[..., :-1].abs()**2
        filters = self._mel_filters(n_mels)
        mel_spec = filters @ magnitudes

        log_spec = torch.clamp(mel_spec, min=1e-10).log10()
        log_spec = torch.maximum(log_spec, log_spec.max() - 8.0)
        log_spec = (log_spec + 4.0) / 4.0
        return log_spec

    def compute_token_num(self, max_feature_len):
        # First, audio goes through encoder:
        # 1. conv1: kernel=3, stride=1, padding=1 -> size unchanged
        # 2. conv2: kernel=3, stride=2, padding=1 -> size/2
        # 3. avg_pooler: kernel=2, stride=2 -> size/2
        max_feature_len = max_feature_len - 2  # remove padding
        encoder_output_dim = (max_feature_len + 1) // 2 // 2  # after conv2 and avg_pooler

        # Then through adaptor (parameters from config file):
        padding = 1
        kernel_size = 3  # from config: audio_encoder_config.kernel_size
        stride = 2  # from config: audio_encoder_config.adapter_stride
        adapter_output_dim = (encoder_output_dim + 2 * padding - kernel_size) // stride + 1
        return adapter_output_dim

    def padding_mels(self, data: List['torch.Tensor']):
        ''' Padding the data into batch data

        Parameters
        ----------
            data: List[Tensor], shape of Tensor (128, T)

        Returns:
        -------
            feats, feats lengths
        '''
        import torch
        from torch.nn.utils.rnn import pad_sequence
        sample = data
        assert isinstance(sample, list)
        feats_lengths = torch.tensor([s.size(1) - 2 for s in sample], dtype=torch.int32)
        feats = [s.t() for s in sample]
        padded_feats = pad_sequence(feats, batch_first=True, padding_value=0)

        return padded_feats.transpose(1, 2), feats_lengths

    def audio_process(self, audio):
        results = []
        mels = []
        for i in range(0, audio.shape[0], 16000 * 25):
            mel = self.log_mel_spectrogram(audio[i:i + 16000 * 25], n_mels=128, padding=479)
            mels.append(mel)
            audio_tokens = '<audio_patch>' * self.compute_token_num(mel.shape[1])
            results.append(f'<audio_start>{audio_tokens}<audio_end>')
        audio_ids = self._tokenize(''.join(results))
        return audio_ids, mels

    def replace_tag(self, media_type: Literal['image', 'video', 'audio'], index: int,
                    inputs: StdTemplateInputs) -> List[Context]:
        assert media_type == 'audio'
        return ['<audio_patch>']

    def _encode(self, inputs: StdTemplateInputs) -> Dict[str, Any]:
        encoded = super()._encode(inputs)
        input_ids = encoded['input_ids']
        labels = encoded['labels']
        loss_scale = encoded.get('loss_scale', None)
        sampling_rate = get_env_args('sampling_rate', int, 16000)
        inputs.audios = load_batch(inputs.audios, partial(self.load_audio, target_rate=sampling_rate))

        audio_token = self._tokenize('<audio_patch>')[0]
        idx_list = findall(input_ids, audio_token)

        if idx_list:
            audio_inputs = []
            mels = []
            for audio in inputs.audios:
                audio_input, mel = self.audio_process(audio)
                audio_inputs.append(audio_input)
                mels.extend(mel)

            def _get_new_audio_tokens(i):
                return audio_inputs[i]

            input_ids, labels, loss_scale = self._extend_tokens(input_ids, labels, loss_scale, idx_list,
                                                                _get_new_audio_tokens)
            encoded['input_ids'] = input_ids  # Add labels to the batch
            encoded['labels'] = labels  # Add labels to the batch
            encoded['loss_scale'] = loss_scale
            encoded['mels'] = mels
            wavs, wav_lens = self.padding_mels(mels)
            # audio_tokens = [151688, 151690, 151689]
            # for audio_token_id in audio_tokens:
            #     labels[labels == audio_token_id] = -100  # Mask image token IDs in labels

        else:
            wavs = None
            wav_lens = None

        encoded['wavs'] = wavs
        encoded['wav_lens'] = wav_lens

        return encoded

    def _data_collator(self, batch: List[Dict[str, Any]], *, padding_to: Optional[int] = None) -> Dict[str, Any]:
        combined_mels = list(itertools.chain.from_iterable([e.get('mels', []) for e in batch]))
        batch_wavs, batch_wav_lens = self.padding_mels(combined_mels) if combined_mels else (None, None)
        res = super()._data_collator(batch, padding_to=padding_to)
        res['wav_lens'] = batch_wav_lens
        res['wavs'] = batch_wavs

        return res


register_template(
    TemplateMeta(
        MLLMTemplateType.step_audio2_mini,
        template_cls=StepAudio2MiniTemplate,
        prefix=[],
        prompt=['<|BOT|>human\n{{QUERY}}<|EOT|><|BOT|>assistant\n'],
        system_prefix=['<|BOT|>system\n{{SYSTEM}}<|EOT|>'],
        chat_sep=['<|EOT|>'],
        suffix=['<|EOT|>'],
    ))

register_template(
    TemplateMeta(
        MLLMTemplateType.step_audio,
        template_cls=StepAudioTemplate,
        prefix=['<s>'],
        prompt=['<|BOT|>human\n{{QUERY}}<|EOT|><|BOT|>assistant\n'],
        system_prefix=['<s><|BOT|>system\n{{SYSTEM}}<|EOT|>'],
        chat_sep=['<|EOT|>'],
        suffix=['<|EOT|>'],
    ))


class Step3VLTemplate(Template):
    use_model = True
    support_padding_free = False
    image_token_id = 151679
    placeholder_tokens = ['<im_patch>']

    def replace_tag(self, media_type: Literal['image', 'video', 'audio'], index: int,
                    inputs: StdTemplateInputs) -> List[Context]:
        assert media_type == 'image'
        return ['<im_patch>']

    def _encode(self, inputs: StdTemplateInputs) -> Dict[str, Any]:
        encoded = super()._encode(inputs)
        input_ids = encoded['input_ids']
        labels = encoded['labels']
        loss_scale = encoded.get('loss_scale', None)

        images = inputs.images
        if images:
            processor = self.processor
            idx_list = findall(input_ids, self.image_token_id)
            splitted_images_data = processor._split_images(images)
            pixel_values_lst = []
            patch_pixel_values_lst = []
            patch_newline_mask_lst = []
            image_repl_ids_lst = []
            num_patches = []
            for raw_img, img_patches, patch_newline_mask in splitted_images_data:
                pixel_values_lst.extend(processor._convert_images_to_pixel_values([raw_img]))
                if len(img_patches) > 0:
                    patch_pixel_values_lst.extend(processor._convert_images_to_pixel_values(img_patches, is_patch=True))
                num_patches.append(len(img_patches))
                _, image_repl_ids = processor._get_image_repl_features(1, len(img_patches), patch_newline_mask)
                image_repl_ids_lst.append(image_repl_ids)
                if patch_newline_mask is not None:
                    patch_newline_mask_lst.extend(patch_newline_mask)

            image_inputs = {
                'pixel_values': torch.cat(pixel_values_lst),
                'num_patches': num_patches,
            }
            if patch_pixel_values_lst:
                image_inputs['patch_pixel_values'] = torch.cat(patch_pixel_values_lst)
            if patch_newline_mask_lst:
                image_inputs['patch_newline_mask'] = torch.tensor(patch_newline_mask_lst, dtype=torch.bool)
            image_inputs = to_float_dtype(image_inputs, self.model_info.torch_dtype)

            def _get_new_tokens(i):
                return image_repl_ids_lst[i]

            input_ids, labels, loss_scale = self._extend_tokens(input_ids, labels, loss_scale, idx_list,
                                                                _get_new_tokens)

        encoded['input_ids'] = input_ids
        encoded['labels'] = labels
        encoded['loss_scale'] = loss_scale
        encoded.update(image_inputs)
        return encoded

    def _post_encode(self, model, inputs: Dict[str, Any]) -> Dict[str, Any]:
        if not self.is_training:
            return inputs

        input_ids = inputs['input_ids']
        # Only one image is supported per sample. # File: modeling_step_vl.py line 319, in _process_image_input
        #           cur_feature.append(image_features[i].view(-1, image_features.shape[-1]))
        pixel_values = inputs.get('pixel_values')
        num_patches = inputs.get('num_patches')
        patch_pixel_values = inputs.get('patch_pixel_values')

        base_model = self.get_base_model(model)
        inputs_embeds = base_model.model.language_model.embed_tokens(input_ids)
        if pixel_values is not None:
            img_inputs = base_model.model._parse_and_validate_image_input(
                pixel_values=pixel_values, num_patches=num_patches, patch_pixel_values=patch_pixel_values)

            # [image embedding or concatenation of image embedding and patch image embedding]
            img_embeddings = base_model.model._process_image_input(img_inputs)

            is_multimodal = input_ids == self.image_token_id
            is_multimodal = is_multimodal.to(inputs_embeds.device)
            bs = is_multimodal.shape[0]
            for i in range(bs):
                assert is_multimodal[i].sum() == img_embeddings[i].shape[0]
            B, L, D = inputs_embeds.shape
            flat_img_embeds = torch.cat(img_embeddings, dim=0)
            flat_mask = is_multimodal.view(-1)
            flat_inputs_embeds = inputs_embeds.view(-1, D)
            flat_inputs_embeds[flat_mask] = flat_img_embeds
            inputs_embeds = flat_inputs_embeds.view(B, L, D)
        return {'inputs_embeds': inputs_embeds}

    def _data_collator_mm_data(self, batch: List[Dict[str, Any]]) -> Dict[str, Any]:
        res = super()._data_collator_mm_data(batch)
        num_patches = self.gather_list(batch, 'num_patches')
        if num_patches:
            res['num_patches'] = num_patches
        patch_pixel_values = [b['patch_pixel_values'] for b in batch if b.get('patch_pixel_values') is not None]
        patch_newline_mask = [b['patch_newline_mask'] for b in batch if b.get('patch_newline_mask') is not None]
        if patch_pixel_values:
            res['patch_pixel_values'] = torch.concat(patch_pixel_values)
            res['patch_newline_mask'] = torch.concat(patch_newline_mask)
        return res


register_template(
    QwenTemplateMeta(
        MLLMTemplateType.step3_vl,
        template_cls=Step3VLTemplate,
        default_system=None,
        is_thinking=True,
        thinking_prefix='<think>\n',
    ))
