"""
TONAS Loader

.. admonition:: Dataset Info
    :class: dropdown

    This dataset contains a music collection of 72 sung excerpts representative of three a cappella singing styles
    (Deblas, and two variants of Martinete). It has been developed within the COFLA research project context.
    The distribution is as follows:
    1. 16 Deblas
    2. 36 Martinete 1
    3. 20 Martinete 2

    This collection was built in the context of a study on similarity and style classification of flamenco a cappella
    singing styles (Tonas) by the flamenco expert Dr. Joaquin Mora, Universidad de Sevilla.

    We refer to (Mora et al. 2010) for a comprehensive description of the considered styles and their musical
    characteristics. All 72 excerpts are monophonic, their average duration is 30 seconds and there is enough
    variability for a proper evaluation of our methods, including a variety of singers, recording conditions,
    presence of percussion, clapping, background voices and noise. We also provide manual melodic transcriptions,
    generated by the COFLA team and Cristina López Gómez.

    The annotations are represented by specifying the value (in this case, Notes and F0) at the related timestamps.
    TONAS' note and F0 annotations also have "Energy" information, which refers to the average energy value through
    all the frames in which a note or a F0 value is comprised.

    Using this dataset:
    TONAS dataset can be obtained upon request. Please refer to this link: https://zenodo.org/record/1290722 to
    request access and follow the indications of the .download() method for a proper storing and organization
    of the TONAS dataset.

    Citing this dataset:
    When TONAS is used for academic research, we would highly appreciate if scientific publications of works partly
    based on the TONAS dataset quote the following publication:
    - Music material: Mora, J., Gomez, F., Gomez, E., Escobar-Borrego, F.J., Diaz-Banez, J.M. (2010). Melodic
    Characterization and Similarity in A Cappella Flamenco Cantes. 11th International Society for Music Information
    Retrieval Conference (ISMIR 2010).
    - Transcriptions: Gomez, E., Bonada, J. (in Press). Towards Computer-Assisted Flamenco Transcription: An
    Experimental Comparison of Automatic Transcription Algorithms As Applied to A Cappella Singing.
    Computer Music Journal.


"""
import csv
import logging
import os
from typing import TextIO, Tuple, Optional

import librosa
import numpy as np

from mirdata import annotations, jams_utils, core, io


BIBTEX = """
Music material:
@inproceedings{tonas_music,
    author = {Mora, Joaquin and Gómez, Francisco and Gómez, Emilia
              and Borrego, Francisco Javier and Díaz-Báñez, José},
    year = {2010},
    month = {01},
    pages = {351-356},
    title = {Characterization and Similarity in A Cappella Flamenco Cantes.}
}

Transcriptions:
@inproceedings{tonas_annotations,
    author = {E. {Gómez} and J. {Bonada}},
    journal = {Computer Music Journal},
    title = {Towards Computer-Assisted Flamenco Transcription: An Experimental 
           Comparison of Automatic Transcription Algorithms as Applied to A 
           Cappella Singing},
    year = {2013},
    volume = {37},
    number = {2},
    pages = {73-90},
    doi = {10.1162/COMJ_a_00180}}
"""

INDEXES = {
    "default": "1.0",
    "test": "1.0",
    "1.0": core.Index(filename="tonas_index_1.0.json"),
}

DOWNLOAD_INFO = """
        PLEASE READ CAREFULLY ALL THE INFORMATION SO YOU DON'T MISS ANY STEP:
        Unfortunately, the TONAS dataset is not available to be shared openly. However,
        you can request access to the dataset in the following link, providing a brief
        explanation of what your are going to use the dataset for:
        ==> https://zenodo.org/record/1290722
        Then, unzip the dataset, change the dataset name to: "tonas" (with lowercase),
        and locate it to {}. If you unzip it into a different path, please remember to set the 
        right data_home when initializing the dataset.
"""

LICENSE_INFO = """
The TONAS dataset is offered free of charge for internal non-commercial use only. You can not redistribute it nor 
modify it. Dataset by COFLA team. Copyright © 2012 COFLA project, Universidad de Sevilla. Distribution rights granted 
to Music Technology Group, Universitat Pompeu Fabra. All Rights Reserved.
"""


class Track(core.Track):
    """TONAS track class

    Args:
        track_id (str): track id of the track
        data_home (str): Local path where the dataset is stored.
            If `None`, looks for the data in the default directory, `~/mir_datasets/TONAS`

    Attributes:
        f0_path (str): local path where f0 melody annotation file is stored
        notes_path(str): local path where notation annotation file is stored
        audio_path(str): local path where audio file is stored
        track_id (str): track id
        singer (str): performing singer (cantaor)
        title (str): title of the track song
        tuning_frequency (float): tuning frequency of the symbolic notation

    Cached Properties:
        f0_automatic (F0Data): automatically extracted f0
        f0_corrected (F0Data): manually corrected f0 annotations
        notes (NoteData): annotated notes

    """

    def __init__(
        self,
        track_id,
        data_home,
        dataset_name,
        index,
        metadata,
    ):
        super().__init__(
            track_id,
            data_home,
            dataset_name,
            index,
            metadata,
        )

        self.f0_path = self.get_path("f0")
        self.notes_path = self.get_path("notes")

        self.audio_path = self.get_path("audio")

    @property
    def style(self):
        return self._track_metadata.get("style")

    @property
    def singer(self):
        return self._track_metadata.get("singer")

    @property
    def title(self):
        return self._track_metadata.get("title")

    @property
    def tuning_frequency(self):
        return _load_tuning_frequency(self.notes_path)

    @property
    def audio(self) -> Tuple[np.ndarray, float]:
        """The track's audio

        Returns:
            * np.ndarray - audio signal
            * float - sample rate

        """
        return load_audio(self.audio_path)

    @core.cached_property
    def f0_automatic(self) -> Optional[annotations.F0Data]:
        return load_f0(self.f0_path, False)

    @core.cached_property
    def f0_corrected(self) -> Optional[annotations.F0Data]:
        return load_f0(self.f0_path, True)

    @property
    def f0(self):
        logging.warning(
            "Deprecation warning: Track.f0 is deprecated and will "
            + "be removed in a future version. Use Track.f0_automatic "
            + "or Track.f0_corrected"
        )
        return self.f0_corrected

    @core.cached_property
    def notes(self) -> Optional[annotations.NoteData]:
        return load_notes(self.notes_path)

    def to_jams(self):
        """Get the track's data in jams format

        Returns:
            jams.JAMS: the track's data in jams format

        """
        return jams_utils.jams_converter(
            audio_path=self.audio_path,
            f0_data=[(self.f0, "pitch_contour")],
            note_data=[(self.notes, "note_hz")],
            metadata=self._track_metadata,
        )


def load_audio(fhandle: str) -> Tuple[np.ndarray, float]:
    """Load a TONAS audio file.

    Args:
        fhandle (str): path to an audio file

    Returns:
        * np.ndarray - the mono audio signal
        * float - The sample rate of the audio file

    """
    return librosa.load(fhandle, sr=44100, mono=True)


# no decorator because of https://github.com/mir-dataset-loaders/mirdata/issues/503
def load_f0(fpath: str, corrected: bool) -> Optional[annotations.F0Data]:
    """Load TONAS f0 annotations

    Args:
        fpath (str): path pointing to f0 annotation file
        corrected (bool): if True, loads manually corrected frequency values
            otherwise, loads automatically extracted frequency values

    Returns:
        F0Data: predominant f0 melody

    """
    times = []
    freqs = []
    freqs_corr = []
    energies = []
    with open(fpath, "r") as fhandle:
        reader = np.genfromtxt(fhandle)
        for line in reader:
            times.append(float(line[0]))
            freqs.append(float(line[2]))
            freqs_corr.append(float(line[3]))
            energies.append(float(line[1]))

    freq_array = np.array(freqs_corr if corrected else freqs, dtype="float")
    energy_array = np.array(energies, dtype="float")
    voicing_array = (freq_array > 0).astype("float")

    return annotations.F0Data(
        np.array(times, dtype="float"),
        "s",
        freq_array,
        "hz",
        voicing_array,
        "binary",
        energy_array,
        "energy",
    )


@io.coerce_to_string_io
def load_notes(fhandle: TextIO) -> Optional[annotations.NoteData]:
    """Load TONAS note data from the annotation files

    Args:
        fhandle (str or file-like): path or file-like object pointing to a notes annotation file

    Returns:
        NoteData: note annotations

    """
    intervals = []
    pitches = []
    energy = []

    reader = csv.reader(fhandle, delimiter=",")
    tuning = next(reader)[0]
    for line in reader:
        intervals.append([line[0], float(line[0]) + float(line[1])])
        # Convert midi value to frequency
        note_hz = _midi_to_hz(float(line[2]), float(tuning))
        pitches.append(note_hz)
        energy.append(float(line[3]))

    note_data = annotations.NoteData(
        np.array(intervals, dtype="float"),
        "s",
        np.array(pitches, dtype="float"),
        "hz",
        np.array(energy, dtype="float"),
        "energy",
    )

    return note_data


@io.coerce_to_string_io
def _load_tuning_frequency(fhandle: TextIO) -> float:
    """Load tuning frequency of the track with re

    Args:
        fhandle (str or file-like): path or file-like object pointing to a notes annotation file

    Returns:
        tuning_frequency (float): returns new tuning frequency considering the deviation

    """

    # Compute tuning frequency
    cents_deviation = float(next(csv.reader(fhandle, delimiter=","))[0])
    tuning_frequency = 440 * (
        2 ** (cents_deviation / 1200)
    )  # Frequency of A (common value is 440Hz)

    return tuning_frequency


def _midi_to_hz(midi_note, tuning_deviation):
    """Function to convert MIDI to Hz with certain tuning freq

    Args:
        midi_note (float): note represented in midi value
        tuning_deviation (float): deviation in cents with respect to 440Hz

    Returns:
        (float): note in Hz considering the new tuning frequency

    """
    tuning_frequency = 440 * (
        2 ** (tuning_deviation / 1200)
    )  # Frequency of A (common value is 440Hz)
    return (tuning_frequency / 32) * (2 ** ((midi_note - 9) / 12))


@core.docstring_inherit(core.Dataset)
class Dataset(core.Dataset):
    """
    The TONAS dataset
    """

    def __init__(self, data_home=None, version="default"):
        super().__init__(
            data_home,
            version,
            name="tonas",
            track_class=Track,
            bibtex=BIBTEX,
            indexes=INDEXES,
            download_info=DOWNLOAD_INFO,
            license_info=LICENSE_INFO,
        )

    @core.cached_property
    def _metadata(self):
        metadata_path = os.path.join(self.data_home, "TONAS-Metadata.txt")
        if not os.path.exists(metadata_path):
            raise FileNotFoundError("Metadata not found. Did you run .download()?")

        metadata = {}
        with open(metadata_path, "r", errors="ignore") as f:
            reader = csv.reader(
                (x.replace("\0", "") for x in f), delimiter="\t"
            )  # Fix wrong byte
            for line in reader:
                if line:  # Do not consider empty lines
                    index = line[0].replace(".wav", "")
                    metadata[index] = {
                        "style": line[1],
                        "title": line[2],
                        "singer": line[3],
                    }

        return metadata

    @core.copy_docs(load_audio)
    def load_audio(self, *args, **kwargs):
        return load_audio(*args, **kwargs)

    @core.copy_docs(load_f0)
    def load_f0(self, *args, **kwargs):
        return load_f0(*args, **kwargs)

    @core.copy_docs(load_notes)
    def load_notes(self, *args, **kwargs):
        return load_notes(*args, **kwargs)
