"""
Functions for loading in annotations from files in different formats.
"""

import contextlib
import numpy as np
import re
import warnings
import scipy.io.wavfile
import six

from . import util
from . import key
from . import tempo


@contextlib.contextmanager
def _open(file_or_str, **kwargs):
    '''Either open a file handle, or use an existing file-like object.

    This will behave as the `open` function if `file_or_str` is a string.

    If `file_or_str` has the `read` attribute, it will return `file_or_str`.

    Otherwise, an `IOError` is raised.
    '''
    if hasattr(file_or_str, 'read'):
        yield file_or_str
    elif isinstance(file_or_str, six.string_types):
        with open(file_or_str, **kwargs) as file_desc:
            yield file_desc
    else:
        raise IOError('Invalid file-or-str object: {}'.format(file_or_str))


def load_delimited(filename, converters, delimiter=r'\s+'):
    r"""Utility function for loading in data from an annotation file where columns
    are delimited.  The number of columns is inferred from the length of
    the provided converters list.

    Examples
    --------
    >>> # Load in a one-column list of event times (floats)
    >>> load_delimited('events.txt', [float])
    >>> # Load in a list of labeled events, separated by commas
    >>> load_delimited('labeled_events.csv', [float, str], ',')

    Parameters
    ----------
    filename : str
        Path to the annotation file
    converters : list of functions
        Each entry in column ``n`` of the file will be cast by the function
        ``converters[n]``.
    delimiter : str
        Separator regular expression.
        By default, lines will be split by any amount of whitespace.

    Returns
    -------
    columns : tuple of lists
        Each list in this tuple corresponds to values in one of the columns
        in the file.

    """
    # Initialize list of empty lists
    n_columns = len(converters)
    columns = tuple(list() for _ in range(n_columns))

    # Create re object for splitting lines
    splitter = re.compile(delimiter)

    # Note: we do io manually here for two reasons.
    #   1. The csv module has difficulties with unicode, which may lead
    #      to failures on certain annotation strings
    #
    #   2. numpy's text loader does not handle non-numeric data
    #
    with _open(filename, mode='r') as input_file:
        for row, line in enumerate(input_file, 1):
            # Split each line using the supplied delimiter
            data = splitter.split(line.strip(), n_columns - 1)

            # Throw a helpful error if we got an unexpected # of columns
            if n_columns != len(data):
                raise ValueError('Expected {} columns, got {} at '
                                 '{}:{:d}:\n\t{}'.format(n_columns, len(data),
                                                         filename, row, line))

            for value, column, converter in zip(data, columns, converters):
                # Try converting the value, throw a helpful error on failure
                try:
                    converted_value = converter(value)
                except:
                    raise ValueError("Couldn't convert value {} using {} "
                                     "found at {}:{:d}:\n\t{}".format(
                                         value, converter.__name__, filename,
                                         row, line))
                column.append(converted_value)

    # Sane output
    if n_columns == 1:
        return columns[0]
    else:
        return columns


def load_events(filename, delimiter=r'\s+'):
    r"""Import time-stamp events from an annotation file.  The file should
    consist of a single column of numeric values corresponding to the event
    times. This is primarily useful for processing events which lack duration,
    such as beats or onsets.

    Parameters
    ----------
    filename : str
        Path to the annotation file
    delimiter : str
        Separator regular expression.
        By default, lines will be split by any amount of whitespace.

    Returns
    -------
    event_times : np.ndarray
        array of event times (float)

    """
    # Use our universal function to load in the events
    events = load_delimited(filename, [float], delimiter)
    events = np.array(events)
    # Validate them, but throw a warning in place of an error
    try:
        util.validate_events(events)
    except ValueError as error:
        warnings.warn(error.args[0])

    return events


def load_labeled_events(filename, delimiter=r'\s+'):
    r"""Import labeled time-stamp events from an annotation file.  The file should
    consist of two columns; the first having numeric values corresponding to
    the event times and the second having string labels for each event.  This
    is primarily useful for processing labeled events which lack duration, such
    as beats with metric beat number or onsets with an instrument label.

    Parameters
    ----------
    filename : str
        Path to the annotation file
    delimiter : str
        Separator regular expression.
        By default, lines will be split by any amount of whitespace.

    Returns
    -------
    event_times : np.ndarray
        array of event times (float)
    labels : list of str
        list of labels

    """
    # Use our universal function to load in the events
    events, labels = load_delimited(filename, [float, str], delimiter)
    events = np.array(events)
    # Validate them, but throw a warning in place of an error
    try:
        util.validate_events(events)
    except ValueError as error:
        warnings.warn(error.args[0])

    return events, labels


def load_intervals(filename, delimiter=r'\s+'):
    r"""Import intervals from an annotation file.  The file should consist of two
    columns of numeric values corresponding to start and end time of each
    interval.  This is primarily useful for processing events which span a
    duration, such as segmentation, chords, or instrument activation.

    Parameters
    ----------
    filename : str
        Path to the annotation file
    delimiter : str
        Separator regular expression.
        By default, lines will be split by any amount of whitespace.

    Returns
    -------
    intervals : np.ndarray, shape=(n_events, 2)
        array of event start and end times

    """
    # Use our universal function to load in the events
    starts, ends = load_delimited(filename, [float, float], delimiter)
    # Stack into an interval matrix
    intervals = np.array([starts, ends]).T
    # Validate them, but throw a warning in place of an error
    try:
        util.validate_intervals(intervals)
    except ValueError as error:
        warnings.warn(error.args[0])

    return intervals


def load_labeled_intervals(filename, delimiter=r'\s+'):
    r"""Import labeled intervals from an annotation file.  The file should consist
    of three columns: Two consisting of numeric values corresponding to start
    and end time of each interval and a third corresponding to the label of
    each interval.  This is primarily useful for processing events which span a
    duration, such as segmentation, chords, or instrument activation.

    Parameters
    ----------
    filename : str
        Path to the annotation file
    delimiter : str
        Separator regular expression.
        By default, lines will be split by any amount of whitespace.

    Returns
    -------
    intervals : np.ndarray, shape=(n_events, 2)
        array of event start and end time
    labels : list of str
        list of labels

    """
    # Use our universal function to load in the events
    starts, ends, labels = load_delimited(filename, [float, float, str],
                                          delimiter)
    # Stack into an interval matrix
    intervals = np.array([starts, ends]).T
    # Validate them, but throw a warning in place of an error
    try:
        util.validate_intervals(intervals)
    except ValueError as error: