import gc
import os.path

import numpy as np
import parselmouth
import torch
import pyworld
import torchcrepe
from scipy import signal
from torch import Tensor


def get_f0_crepe_computation(
        x,
        f0_min,
        f0_max,
        p_len,
        sr,
        hop_length=128,
        # 512 before. Hop length changes the speed that the voice jumps to a different dramatic pitch. Lower hop lengths means more pitch accuracy but longer inference time.
        model="full",  # Either use crepe-tiny "tiny" or crepe "full". Default is full
):
    x = x.astype(np.float32)  # fixes the F.conv2D exception. We needed to convert double to float.
    x /= np.quantile(np.abs(x), 0.999)
    torch_device = 'cuda' if torch.cuda.is_available() else 'cpu'
    audio = torch.from_numpy(x).to(torch_device, copy=True)
    audio = torch.unsqueeze(audio, dim=0)
    if audio.ndim == 2 and audio.shape[0] > 1:
        audio = torch.mean(audio, dim=0, keepdim=True).detach()
    audio = audio.detach()
    # print("Initiating prediction with a crepe_hop_length of: " + str(hop_length))
    pitch: torch.Tensor = torchcrepe.predict(
        audio,
        sr,
        hop_length,
        f0_min,
        f0_max,
        model,
        batch_size=hop_length * 2,
        device=torch_device,
        pad=True
    )
    p_len = p_len or x.shape[0] // hop_length
    # Resize the pitch for final f0
    source = np.array(pitch.squeeze(0).cpu().float().numpy())
    source[source < 0.001] = np.nan
    target = np.interp(
        np.arange(0, len(source) * p_len, len(source)) / p_len,
        np.arange(0, len(source)),
        source
    )
    f0 = np.nan_to_num(target)
    return f0  # Resized f0


def get_mangio_crepe_f0(x, f0_min, f0_max, p_len, sr, crepe_hop_length, model='full'):
    # print("Performing crepe pitch extraction. (EXPERIMENTAL)")
    # print("CREPE PITCH EXTRACTION HOP LENGTH: " + str(crepe_hop_length))
    x = x.astype(np.float32)
    x /= np.quantile(np.abs(x), 0.999)
    torch_device_index = 0
    torch_device = None
    if torch.cuda.is_available():
        torch_device = torch.device(f"cuda:{torch_device_index % torch.cuda.device_count()}")
    elif torch.backends.mps.is_available():
        torch_device = torch.device("mps")
    else:
        torch_device = torch.device("cpu")
    audio = torch.from_numpy(x).to(torch_device, copy=True)
    audio = torch.unsqueeze(audio, dim=0)
    if audio.ndim == 2 and audio.shape[0] > 1:
        audio = torch.mean(audio, dim=0, keepdim=True).detach()
    audio = audio.detach()
    # print(
    #     "Initiating f0 Crepe Feature Extraction with an extraction_crepe_hop_length of: " +
    #     str(crepe_hop_length)
    # )
    # Pitch prediction for pitch extraction
    pitch: Tensor = torchcrepe.predict(
        audio,
        sr,
        crepe_hop_length,
        f0_min,
        f0_max,
        model,
        batch_size=crepe_hop_length * 2,
        device=torch_device,
        pad=True
    )
    p_len = p_len or x.shape[0] // crepe_hop_length
    # Resize the pitch
    source = np.array(pitch.squeeze(0).cpu().float().numpy())
    source[source < 0.001] = np.nan
    target = np.interp(
        np.arange(0, len(source) * p_len, len(source)) / p_len,
        np.arange(0, len(source)),
        source
    )
    return np.nan_to_num(target)


def pitch_extract(f0_method, x, f0_min, f0_max, p_len, time_step, sr, window, crepe_hop_length, filter_radius=3):
    f0s = []
    f0 = np.zeros(p_len)
    for method in f0_method if isinstance(f0_method, list) else [f0_method]:
        if method == "pm":
            f0 = (
                parselmouth.Sound(x, sr)
                .to_pitch_ac(
                    time_step=time_step / 1000,
                    voicing_threshold=0.6,
                    pitch_floor=f0_min,
                    pitch_ceiling=f0_max,
                )
                .selected_array["frequency"]
            )
            pad_size = (p_len - len(f0) + 1) // 2
            if pad_size > 0 or p_len - len(f0) - pad_size > 0:
                f0 = np.pad(
                    f0, [[pad_size, p_len - len(f0) - pad_size]], mode="constant"
                )
        elif method in ['harvest', 'dio']:
            if method == 'harvest':
                f0, t = pyworld.harvest(
                    x.astype(np.double),
                    fs=sr,
                    f0_ceil=f0_max,
                    f0_floor=f0_min,
                    frame_period=10,
                )
            elif method == "dio":
                f0, t = pyworld.dio(
                    x.astype(np.double),
                    fs=sr,
                    f0_ceil=f0_max,
                    f0_floor=f0_min,
                    frame_period=10,
                )
            f0 = pyworld.stonemask(x.astype(np.double), f0, t, sr)
        elif method == "torchcrepe":
            f0 = get_f0_crepe_computation(x, f0_min, f0_max, p_len, sr, crepe_hop_length)
        elif method == "torchcrepe tiny":
            f0 = get_f0_crepe_computation(x, f0_min, f0_max, p_len, sr, crepe_hop_length, "tiny")
        elif method == "mangio-crepe":
            f0 = get_mangio_crepe_f0(x, f0_min, f0_max, p_len, sr, crepe_hop_length)
        elif method == "mangio-crepe tiny":
            f0 = get_mangio_crepe_f0(x, f0_min, f0_max, p_len, sr, crepe_hop_length, 'tiny')
        elif method == "rmvpe":
            rmvpe_model_path = os.path.join('data', 'models', 'rmvpe')
            rmvpe_model_file = os.path.join(rmvpe_model_path, 'rmvpe.pt')
            if not os.path.isfile(rmvpe_model_file):
                import huggingface_hub
                rmvpe_model_file = huggingface_hub.hf_hub_download('lj1995/VoiceConversionWebUI', 'rmvpe.pt', local_dir=rmvpe_model_path, local_dir_use_symlinks=False)

            from modules.voice_conversion.rvc.rmvpe import RMVPE
            print("loading rmvpe model")
            model_rmvpe = RMVPE(rmvpe_model_file, is_half=True, device=None)
            f0 = model_rmvpe.infer_from_audio(x, thred=0.03)
            del model_rmvpe
            torch.cuda.empty_cache()
            gc.collect()
        f0s.append(f0)

    if not f0s:
        f0s = [f0]

    f0s_new = []
    for f0_val in f0s:
        _len = f0_val.shape[0]
        if _len == p_len:
            f0s_new.append(f0)
            continue
        if _len > p_len:
            f0 = f0[:p_len]
            f0s_new.append(f0)
            continue
        if _len < p_len:
            print('WARNING: len < p_len, skipping this f0')


    f0 = np.nanmedian(np.stack(f0s_new, axis=0), axis=0)

    if filter_radius >= 2:
        f0 = signal.medfilt(f0, filter_radius)

    return f0
