import warnings
from pathlib import Path

import argbind
import numpy as np
import torch
from audiotools import AudioSignal
from tqdm import tqdm

from dac.utils import load_model

warnings.filterwarnings("ignore", category=UserWarning)


@torch.no_grad()
@torch.inference_mode()
def process(
    artifacts: dict,
    device: str,
    generator: torch.nn.Module,
    preserve_sample_rate: bool,
) -> AudioSignal:
    """Decode encoded audio. The `artifacts` contain codes from chunked windows
    of the original audio signal. The codes are decoded one by one and windows are trimmed and concatenated together to form the final output.

    Parameters
    ----------
    artifacts : dict
        Dictionary of artifacts with the following keys:
        - codes: the quantized codes
        - metadata: dictionary with following keys
            - original_db: the loudness of the input signal
            - overlap_hop_duration: the hop duration of the overlap window
            - original_length: the original length of the input signal
            - is_overlap: whether the input signal was overlapped
            - batch_size: the batch size of the input signal
            - channels: the number of channels of the input signal
            - original_sr: the original sample rate of the input signal
    device : str
        Device to use
    generator : torch.nn.Module
        Generator to decode with.
    preserve_sample_rate : bool
        If True, return audio will have the same sample rate as the original
        encoded audio. If False, return audio will have the sample rate of the
        generator.

    Returns
    -------
    AudioSignal
    """
    if isinstance(generator, torch.nn.DataParallel):
        generator = generator.module
    audio_signal = AudioSignal(
        artifacts["codes"].astype(np.int64), generator.sample_rate
    )
    metadata = artifacts["metadata"]

    # Decode chunks
    output = []
    for i in range(audio_signal.batch_size):
        signal_from_batch = AudioSignal(
            audio_signal.audio_data[i, ...], audio_signal.sample_rate, device=device
        )
        z_q = generator.quantizer.from_codes(signal_from_batch.audio_data)[0]
        audio = generator.decode(z_q)["audio"].cpu()
        output.append(audio)

    output = torch.cat(output, dim=0)
    output_signal = AudioSignal(output, generator.sample_rate)

    # Overlap and add
    if metadata["is_overlap"]:
        boundary = int(metadata["overlap_hop_duration"] * generator.sample_rate / 2)
        # remove window overlap
        output_signal.trim(boundary, boundary)
        output_signal.audio_data = output_signal.audio_data.reshape(
            metadata["batch_size"], metadata["channels"], -1
        )
        # remove padding
        output_signal.trim(boundary, boundary)

    # Restore loudness and truncate to original length
    output_signal.ffmpeg_loudness()
    output_signal = output_signal.normalize(metadata["original_db"])
    output_signal.truncate_samples(metadata["original_length"])

    if preserve_sample_rate:
        output_signal = output_signal.ffmpeg_resample(metadata["original_sr"])

    return output_signal.to("cpu")


@argbind.bind(group="decode", positional=True, without_prefix=True)
@torch.inference_mode()
@torch.no_grad()
def decode(
    input: str,
    output: str = "",
    weights_path: str = "",
    model_tag: str = "latest",
    preserve_sample_rate: bool = False,
    device: str = "cuda",
    model_type: str = "44khz",
):
    """Decode audio from codes.

    Parameters
    ----------
    input : str
        Path to input directory or file
    output : str, optional
        Path to output directory, by default "".
        If `input` is a directory, the directory sub-tree relative to `input` is re-created in `output`.
    weights_path : str, optional
        Path to weights file, by default "". If not specified, the weights file will be downloaded from the internet using the
        model_tag and model_type.
    model_tag : str, optional
        Tag of the model to use, by default "latest". Ignored if `weights_path` is specified.
    preserve_sample_rate : bool, optional
        If True, return audio will have the same sample rate as the original
    device : str, optional
        Device to use, by default "cuda". If "cpu", the model will be loaded on the CPU.
    model_type : str, optional
        The type of model to use. Must be one of "44khz", "24khz", or "16khz". Defaults to "44khz". Ignored if `weights_path` is specified.
    """
    generator = load_model(
        tag=model_tag,
        load_path=weights_path,
        model_type=model_type,
    )
    generator.to(device)
    generator.eval()

    # Find all .dac files in input directory
    _input = Path(input)
    input_files = list(_input.glob("**/*.dac"))

    # If input is a .dac file, add it to the list
    if _input.suffix == ".dac":
        input_files.append(_input)

    # Create output directory
    output = Path(output)
    output.mkdir(parents=True, exist_ok=True)

    for i in tqdm(range(len(input_files)), desc=f"Decoding files"):
        # Load file
        artifacts = np.load(input_files[i], allow_pickle=True)[()]

        # Reconstruct audio from codes
        recons = process(artifacts, device, generator, preserve_sample_rate)

        # Compute output path
        relative_path = input_files[i].relative_to(input)
        output_dir = output / relative_path.parent
        if not relative_path.name:
            output_dir = output
            relative_path = input_files[i]
        output_name = relative_path.with_suffix(".wav").name
        output_path = output_dir / output_name
        output_path.parent.mkdir(parents=True, exist_ok=True)

        # Write to file
        recons.write(output_path)


if __name__ == "__main__":
    args = argbind.parse_args()
    with argbind.scope(args):
        decode()
