import os
import torch
from torch import nn, optim
from torch.utils.data import DataLoader, Dataset
import torchaudio
import librosa
import noisereduce as nr
import numpy as np
import crepe
import madmom
from music21 import stream, note, converter, instrument
from transformers import Wav2Vec2ForCTC, Wav2Vec2Tokenizer, HubertForCTC, HubertTokenizer
from scipy import signal
import openunmix
from demucs import pretrained
import openai
import yamnet as yamnet_model
import openl3
from onsets_and_frames import NonBinaryClassification, inference
import pydub
import parselmouth
from spleeter.separator import Separator
import soundfile as sf
import time
import tonic
import logging
import asyncio
from diskcache import Cache

# Загрузка ключа API из переменных среды
openai.api_key = os.environ.get('OPENAI_API_KEY')

# Настройка логирования
logging.basicConfig(level=logging.INFO)

# Настройка кэширования
cache = Cache('audio_cache')

# Конфигурационные параметры
config = {
    "output_dir": "output",
    "noise_reduction_level": 5,
    "pitch_shift": 2,
    "speed_change": 1.5,
    "batch_size": 16,
    "epochs": 10,
    "target_format": "wav"
}

# Асинхронная функция для конвейера обработки
async def preprocess_audio(file_path):
    """
    Конвейер обработки аудио
    """
    try:
        logging.info(f"Loading and preprocessing audio from {file_path}")
        y, sr = librosa.load(file_path, sr=None)

        if (y, sr) in cache:
            logging.info("Using cached preprocessed audio")
            return cache[(y, sr)]

        y = librosa.util.normalize(y)
        y = nr.reduce_noise(y, sr)
        y = signal.wiener(y)

        cache[(y, sr)] = (y, sr)
        logging.info("Audio preprocessing completed")

        return y, sr
    except Exception as e:
        logging.error(f"Error preprocessing audio: {e}")
        return None, None

async def extract_features_task(y, sr):
    """
    Асинхронная задача для извлечения признаков
    """
    try:
        cqt = librosa.cqt(y, sr=sr)
        cqt_db = librosa.amplitude_to_db(np.abs(cqt), ref=np.max)

        # Pitch detection using CREPE
        _, frequency, confidence, _ = crepe.predict(y, sr)

        return cqt_db, frequency, confidence
    except Exception as e:
        logging.error(f"Error extracting features: {e}")
        return None, None, None

def transformer_features_wav2vec2(y, sr):
    """
    Асинхронная задача для извлечения признаков с использованием Wav2Vec2
    """
    try:
        tokenizer = Wav2Vec2Tokenizer.from_pretrained("facebook/wav2vec2-base-960h")
        model = Wav2Vec2ForCTC.from_pretrained("facebook/wav2vec2-base-960h")

        input_values = tokenizer(y, return_tensors="pt", padding=True).input_values
        logits = model(input_values).logits
        predicted_ids = torch.argmax(logits, dim=-1)

        transcription = tokenizer.batch_decode(predicted_ids)[0]
        return transcription
    except Exception as e:
        logging.error(f"Error extracting transformer features with Wav2Vec2: {e}")
        return None

def transformer_features_hubert(y, sr):
    """
    Асинхронная задача для извлечения признаков с использованием HuBERT
    """
    try:
        tokenizer = HubertTokenizer.from_pretrained("facebook/hubert-large-ls960-ft")
        model = HubertForCTC.from_pretrained("facebook/hubert-large-ls960-ft")

        input_values = tokenizer(y, return_tensors="pt", padding=True).input_values
        logits = model(input_values).logits
        predicted_ids = torch.argmax(logits, dim=-1)

        transcription = tokenizer.batch_decode(predicted_ids)[0]
        return transcription
    except Exception as e:
        logging.error(f"Error extracting transformer features with Hubert: {e}")
        return None

def generate_text_descriptions_with_chatgpt(prompt):
    """
    Генерация текста с использованием GPT-4
    """
    try:
        response = openai.Completion.create(
            model="gpt-4",
            prompt=prompt,
            max_tokens=150  # Увеличиваем количество токенов для получения более детального ответа
        )
        return response.choices[0].text.strip()
    except Exception as e:
        logging.error(f"Error generating text descriptions with GPT-4: {e}")
        return None

class AudioPipeline:
    def __init__(self, config):
        self.config = config

    def convert_audio_format(self, file_path, target_format="wav"):
        """
        Конвертация аудиоформата
        """
        try:
            audio = pydub.AudioSegment.from_file(file_path)
            new_file_path = file_path.replace(file_path.split('.')[-1], target_format)
            audio.export(new_file_path, format=target_format)
            logging.info(f"Audio format converted to {target_format}")
            return new_file_path
        except Exception as e:
            logging.error(f"Error converting audio format: {e}")
            return None

    def play_audio(self, file_path):
        try:
            data, sr = sf.read(file_path)
            import sounddevice as sd
            sd.play(data, sr)
            sd.wait()
        except Exception as e:
            logging.error(f"Error playing audio: {e}")

    async def run_pipeline(self, file_path):
        """
        Запуск конвейера обработки
        """
        # Конвертация аудиоформата
        converted_file_path = self.convert_audio_format(file_path, self.config["target_format"])

        # Предварительная обработка
        y, sr = await preprocess_audio(converted_file_path)
        if y is None or sr is None:
            logging.error("Error in audio preprocessing step")
            return

        # Запуск задач извлечения признаков в фоновом режиме
        features_task = extract_features_task(y, sr)

        # Запуск Преобразующих Моделей
        transcription_wav2vec2_task = asyncio.to_thread(transformer_features_wav2vec2, y, sr)
        transcription_hubert_task = asyncio.to_thread(transformer_features_hubert, y, sr)

        # Ожидание завершения задач
        features_result = await features_task
        transcription_wav2vec2 = await transcription_wav2vec2_task
        transcription_hubert = await transcription_hubert_task

        cqt_db, frequency, confidence = features_result

        logging.info(f"Feature extraction complete: {cqt_db.shape}, {len(frequency)}, {transcription_wav2vec2}, {transcription_hubert}")

        # Запуск дополнительных задач
        additional_features = await asyncio.to_thread(self.additional_features_essentia, y, sr)

        # Сепарация источников
        sources_openunmix = await asyncio.to_thread(self.source_separation_openunmix, y, sr)
        sources_demucs_v3 = await asyncio.to_thread(self.source_separation_demucs_v3, y, sr)
        vocals_path, accompaniment_path = await asyncio.to_thread(self.source_separation_spleeter, converted_file_path)

        logging.info(f"Source separation complete: OpenUnmix: {sources_openunmix}, Demucs V3: {sources_demucs_v3}, Spleeter: {vocals_path}, {accompaniment_path}")

        # Анализ высоты тона с использованием Parselmouth
        pitch_analysis = await asyncio.to_thread(self.analyze_pitch_with_parselmouth, converted_file_path)
        logging.info(f"Pitch analysis (Parselmouth): {pitch_analysis}")

        # Оптимизация
        reduced_noise_path = self.reduce_noise(converted_file_path, self.config["noise_reduction_level"])
        modified_audio_path = self.change_timbre_and_speed(reduced_noise_path, self.config["pitch_shift"], self.config["speed_change"])
        normalized_audio_path = self.normalize_volume_level(modified_audio_path)

        return cqt_db, frequency, confidence, transcription_wav2vec2, transcription_hubert, additional_features, sources_openunmix, sources_demucs_v3, vocals_path, accompaniment_path, pitch_analysis, normalized_audio_path

    def reduce_noise(self, file_path, noise_reduction_level=5):
        try:
            audio = pydub.AudioSegment.from_file(file_path)
            reduced_noise_audio = audio - noise_reduction_level
            reduced_noise_file_path = file_path.replace(".wav", "_reduced_noise.wav")
            reduced_noise_audio.export(reduced_noise_file_path, format="wav")
            logging.info(f"Noise reduced and saved to {reduced_noise_file_path}")
            return reduced_noise_file_path
        except Exception as e:
            logging.error(f"Error reducing noise: {e}")
            return None

    def change_timbre_and_speed(self, file_path, pitch_shift=2, speed_change=1.5):
        try:
            audio = pydub.AudioSegment.from_file(file_path)
            new_timbre_audio = audio._spawn(audio.raw_data, overrides={'frame_rate': int(audio.frame_rate * pitch_shift)})
            new_timbre_speed_audio = new_timbre_audio.speedup(playback_speed=speed_change)
            new_file_path = file_path.replace(".wav", "_modified.wav")
            new_timbre_speed_audio.export(new_file_path, format="wav")
            logging.info(f"Timbre and speed changed and saved to {new_file_path}")
            return new_file_path
        except Exception as e:
            logging.error(f"Error changing timbre and speed: {e}")
            return None

    def normalize_volume_level(self, file_path):
        try:
            audio = pydub.AudioSegment.from_file(file_path)
            normalized_audio = audio.apply_gain(-audio.dBFS)
            normalized_file_path = file_path.replace(".wav", "_normalized.wav")
            normalized_audio.export(normalized_file_path, format="wav")
            logging.info(f"Volume level normalized and saved to {normalized_file_path}")
            return normalized_file_path
        except Exception as e:
            logging.error(f"Error normalizing volume level: {e}")
            return None

    def analyze_pitch_with_parselmouth(self, file_path):
        try:
            snd = parselmouth.Sound(file_path)
            pitch = snd.to_pitch()
            pitch_values = pitch.selected_array['frequency']
            return pitch_values
        except Exception as e:
            logging.error(f"Error analyzing pitch with Parselmouth: {e}")
            return None

    def additional_features_essentia(self, y, sr):
        try:
            from essentia.standard import MusicExtractor, MonoLoader
            loader = MonoLoader(sampleRate=sr)
            y = loader(y)
            extractor = MusicExtractor(lowlevelStats=['mean', 'stdev'])
            features, _ = extractor(y)
            return features
        except Exception as e:
            logging.error(f"Error extracting additional features with Essentia: {e}")
            return None

    def source_separation_openunmix(self, y, sr):
        try:
            separator = openunmix.separate(
                audio_signal=y.tolist(),
                sample_rate=sr,
                targets=["vocals", "drums", "bass", "other"]
            )
            return {
                "vocals": separator["vocals"],
                "drums": separator["drums"],
                "bass": separator["bass"],
                "other": separator["other"],
            }
        except Exception as e:
            logging.error(f"Error in source separation with OpenUnmix: {e}")
            return None

    def source_separation_demucs_v3(self, y, sr):
        try:
            model = pretrained.get_model('demucs')

            waveform = torch.tensor(y).unsqueeze(0)
            waveform = waveform.to(model.device)

            sources = model(waveform)
            return sources
        except Exception as e:
            logging.error(f"Error in source separation with Demucs v3: {e}")
            return None

    def source_separation_spleeter(self, file_path):
        try:
            separator = Separator('spleeter:2stems')
            separator.separate_to_file(file_path, output_path='output')
            return 'output/vocals.wav', 'output/accompaniment.wav'
        except Exception as e:
            logging.error(f"Error in source separation with Spleeter: {e}")
            return None, None

    def recognize_instruments(self, y, sr):
        try:
            params = yamnet_model.Params()
            yamnet = yamnet_model.yamnet(params)
            yamnet.load_weights('yamnet.h5')

            waveform = np.reshape(y, [len(y), 1])

            class_scores, embeddings, spectrogram = yamnet(waveform)

            classes = yamnet_model.class_names(params)
            top_classes = np.argsort(class_scores, axis=-1)[0, -5:][::-1]
            recognized_classes = [classes[i] for i in top_classes]

            return recognized_classes
        except Exception as e:
            logging.error(f"Error recognizing instruments: {e}")
            return None

    def recognize_genre(self, y, sr):
        try:
            features = self.extract_features(y, sr)
            genre_description = generate_text_descriptions_with_chatgpt(f"Recognize the genre based on these features: {features}")
            return genre_description
        except Exception as e:
            logging.error(f"Error recognizing genre: {e}")
            return None

    # Построение MusicXML
    def create_musicxml(self, frequencies, beats, frames, onsets, offsets, text_descriptions, instruments):
        try:
            score = stream.Score()
            part = stream.Part()

            # Чтобы добавить все инструменты, которые доступны в music21
instrument_classes = [cls for cls in dir(instrument) if isinstance(getattr(instrument, cls), type) and issubclass(getattr(instrument, cls), instrument.Instrument)]

for freq, beat, frame, onset, offset, description, inst in zip(frequencies, beats, frames, onsets, offsets, text_descriptions, instruments):
    if onset > 0:
        n = note.Note()
        n.pitch.frequency = freq
        n.quarterLength = beat
        n.offset = offset
        n.addLyric(description)

        # Добавление инструмента к ноте
        if hasattr(instrument, inst):
            instrument_class = getattr(instrument, inst)
            part.append(instrument_class())
        else:
            n.addLyric("Unknown Instrument")

        part.append(n)

score.append(part)
return score

def visualize_score(self, score, output_image_path):
    """
    Визуализация партитуры и сохранение в виде изображения
    """
    try:
        s = converter.parse(score)
        s.show('text')  # Показать партитуру в текстовом виде (можно удалить, если не нужно)

        fp = s.write('musicxml.png', fp=output_image_path)
        logging.info(f'Партитура сохранена в {fp}')
    except Exception as e:
        logging.error(f"Error visualizing score: {e}")

class MusicDataset(Dataset):
    def __init__(self, features, labels):
        self.features = features
        self.labels = labels

    def __len__(self):
        return len(self.features)

    def __getitem__(self, idx):
        return self.features[idx], self.labels[idx]

def train_cnn_model(train_loader, in_channels, out_channels, epochs=10):
    model = AudioCNN(in_channels, out_channels)
    criterion = nn.MSELoss()
    optimizer = optim.Adam(model.parameters(), lr=0.001)

    for epoch in range(epochs):
        for inputs, labels in train_loader:
            # Возможно, добавить использование GPU
            inputs, labels = inputs.to(device), labels.to(device)
            optimizer.zero_grad()
            outputs = model(inputs)
            loss = criterion(outputs, labels)
            loss.backward()
            optimizer.step()

            logging.info(f"Epoch {epoch+1}/{epochs}, Loss: {loss.item()}")

    return model

def generate_text_descriptions(features):
    try:
        response = openai.Completion.create(
            model="gpt-4",
            prompt=f"Generate a coherent description based on the following features: {features}",
            max_tokens=100
        )
        return response.choices[0].text.strip()
    except Exception as e:
        logging.error(f"Error generating text descriptions: {e}")
        return None

def modify_notes_with_gpt4(score, action="add", instrument="piano"):
    notes_data = [(n.pitch.frequency, n.quarterLength, n.offset) for n in score.flat.notes]
    prompt = f"Партитура содержит следующие данные нот:\n{notes_data}\n Пожалуйста, предоставьте необходимые изменения, чтобы {('добавить' if action == 'add' else 'удалить')} ноты для инструмента '{instrument}', и обеспечьте соблюдение правильных музыкальных правил. Также проанализируйте сложность композиции."

    try:
        response = openai.Completion.create(
            model="gpt-4",
            prompt=prompt,
            max_tokens=150  # Увеличиваем количество токенов для получения более детального ответа
        )

        modified_notes = response.choices[0].text.strip().split('\n')

        for note_data in modified_notes:
            try:
                note_info = note_data.split(',')
                if len(note_info) == 3:
                    freq, length, offset = map(float, note_info)
                    if action == "add":
                        new_note = note.Note()
                        new_note.pitch.frequency = freq
                        new_note.quarterLength = length
                        new_note.offset = offset
                        new_note.addLyric(instrument)
                        score.append(new_note)
                    else:
                        for n in score.flat.notes:
                            if abs(n.pitch.frequency - freq) < 1e-3 and abs(n.quarterLength - length) < 1e-3 and abs(n.offset - offset) < 1e-3:
                                score.remove(n)
                else:
                    logging.warning(f"Skipping invalid note data: {note_data}")
            except ValueError as ve:
                logging.error(f"Error processing note data: {note_data} -> {ve}")
    except Exception as e:
        logging.error(f"Error generating modifications with GPT-4: {e}")

    return score

def plot_spectrogram(y, sr, output_image_path):
    try:
        import matplotlib.pyplot as plt
        import librosa.display
        
        D = librosa.amplitude_to_db(np.abs(librosa.stft(y)), ref=np.max)
        plt.figure(figsize=(12, 8))
        librosa.display.specshow(D, sr=sr, x_axis='time', y_axis='log')
        plt.colorbar(format='%+2.0f dB')
        plt.title('Spectrogram')
        plt.savefig(output_image_path)
        logging.info(f'Spectrogram saved to {output_image_path}')
        plt.close()
    except Exception as e:
        logging.error(f"Error plotting spectrogram: {e}")

def plot_chromagram(y, sr, output_image_path):
    try:
        import matplotlib.pyplot as plt
        import librosa.display

        chromagram = librosa.feature.chroma_stft(y=y, sr=sr)
        plt.figure(figsize=(12, 8))
        librosa.display.specshow(chromagram, sr=sr, x_axis='time', y_axis='chroma')
        plt.colorbar(format='%+2.0f')
        plt.title('Chromagram')
        plt.savefig(output_image_path)
        logging.info(f'Chromagram saved to {output_image_path}')
        plt.close()
    except Exception as e:
        logging.error(f"Error plotting chromagram: {e}")

def compute_spectral_centroid(y, sr):
    try:
        spectral_centroids = librosa.feature.spectral_centroid(y, sr=sr)[0]
        return spectral_centroids
    except Exception as e:
        logging.error(f"Error computing spectral centroid: {e}")
        return None

def compute_tonality_and_stability(y, sr):
    try:
        chroma_stft = librosa.feature.chroma_stft(y=y, sr=sr)
        key = librosa.estimate_tuning(y=y, sr=sr)
        return key, chroma_stft
    except Exception as e:
        logging.error(f"Error computing tonality and stability: {e}")
        return None, None

def harmonic_rhythm_analysis(y, sr):
    try:
        tempo, beat_frames = librosa.beat.beat_track(y=y, sr=sr)
        chords = librosa.effects.harmonic(y)

        # Анализ ритма с использованием madmom
        proc = madmom.features.beats.RNNBeatProcessor()(y)
        beat_times = madmom.features.beats.DBNBeatTrackingProcessor(beats_per_bar=[3, 4])(proc)

        return tempo, beat_frames, chords, beat_times
    except Exception as e:
        logging.error(f"Error in harmonic rhythm analysis: {e}")
        return None, None, None, None

def main(file_path, output_path, output_image_path):
    # Конфигурирование кэширования данных
    dataset_config = {
        "features": "path_to_features_dataset",
        "labels": "path_to_labels_dataset"
    }

    # Инициализация конвейера и запуск обработки
    audio_pipeline = AudioPipeline(config)
    loop = asyncio.get_event_loop()
    results = loop.run_until_complete(audio_pipeline.run_pipeline(file_path))

    if results:
        # Обработка результатов
        (cqt_db, frequency, confidence, transcription_wav2vec2, transcription_hubert, 
         additional_features, sources_openunmix, sources_demucs_v3, vocals_path, 
         accompaniment_path, pitch_analysis, normalized_audio_path) = results

        logging.info(f"Processing results: {cqt_db.shape}, {len(frequency)}, {transcription_wav2vec2}, {transcription_hubert}")

        # Гармонический анализ и ритм
        tempo, beat_frames, chords, beat_times = harmonic_rhythm_analysis(y, sr)
        if tempo is None or beat_frames is None or chords is None or beat_times is None:
            logging.error("Error in harmonic rhythm analysis")
            return

        # Генерация текстовых описаний музыкальных фрагментов с использованием ChatGPT (GPT-4)
        text_descriptions = generate_text_descriptions(features)
        if text_descriptions is None:
            logging.error("Error generating text descriptions")
            return

        # Распознавание музыкальных инструментов
        instruments = audio_pipeline.recognize_instruments(y, sr)
        if instruments is None:
            logging.error("Error recognizing instruments")
            return

        # Распознавание жанра музыки
        genre_description = audio_pipeline.recognize_genre(y, sr)
        if genre_description is None:
            logging.error("Error recognizing genre")
            return

        # Спектральный центроид
        spectral_centroid = compute_spectral_centroid(y, sr)
        if spectral_centroid is None:
            logging.error("Error computing spectral centroid")
            return

        # Тональность и тональная стабильность
        key, chroma_stft = compute_tonality_and_stability(y, sr)
        if key is None or chroma_stft is None:
            logging.error("Error computing tonality and stability")
            return

        # Создание MusicXML из результатов
        score = audio_pipeline.create_musicxml(frequency, beat_times, frames_vae, onsets_vae, offsets_vae, text_descriptions, instruments)
        if score is None:
            logging.error("Error creating MusicXML")
            return

        # Логика добавления или удаления нот с использованием GPT-4 (ChatGPT)
        score = modify_notes_with_gpt4(score, action="add", instrument="piano")

        # Сохранение и визуализация партитуры в формате MusicXML и изображения
        try:
            score.write('musicxml', fp=output_path)
        except Exception as e:
            logging.error(f"Error writing MusicXML: {e}")
            return

        audio_pipeline.visualize_score(score, output_image_path)

        # Визуализация спектрограммы аудиофайла
        spectrogram_image_path = output_image_path.replace(".png", "_spectrogram.png")
        y, sr = preprocess_audio(file_path)
        plot_spectrogram(y, sr, spectrogram_image_path)

        # Визуализация хромограммы аудиофайла
        chromagram_image_path = output_image_path.replace(".png", "_chromagram.png")
        plot_chromagram(y, sr, chromagram_image_path)

        # Демонстрация воспроизведения аудио
        audio_pipeline.play_audio(normalized_audio_path)

        # Ожидание завершения преобразования, если процесс занял меньше времени, чем длительность аудио
        elapsed_time = time.time() - start_time
        if elapsed_time < duration_sec:
            time.sleep(duration_sec - elapsed_time)

        # Окончательное описание всех источников
        full_description = {
            "Transcription (Wav2Vec2)": transcription_wav2vec2,
            "Transcription (HuBERT)": transcription_hubert,
            "Genre Description (GPT-4)": genre_description,
            "Instrument Recognition (YAMNet)": instruments,
            "Additional Features (Essentia)": additional_features,
            "Pitch Analysis (Parselmouth)": pitch_analysis,
            "Spectral Centroid": spectral_centroid,
            "Key (Tuning)": key,
            "Chroma (Tonality Stability)": chroma_stft
        }

        logging.info("Full Description:")
        for key, value in full_description.items():
            logging.info(f"{key}: {value}")
    else:
        logging.error("No results from audio processing pipeline")

# Запуск скрипта
if __name__ == "__main__":
    file_path = 'path_to_your_mp3_file.mp3'
    output_path = 'output_musicxml_file.xml'
    output_image_path = 'output_image.png'
    start_time = time.time()
    main(file_path, output_path, output_image_path)