

import math
import warnings

import matplotlib.pyplot as plt
from matplotlib.lines import Line2D
from math import floor

from showy.preprocess_utils import preprocess_layout
from showy.validation import validate_layout


# TODO: add export of data
# TODO: layout merging
# TODO: add schema validation


def showy(layout, dataframe, show=True, preprocess_fnc=preprocess_layout):
    """It displays the desired graphs described by the provided layout
    thanks to the provided key-value object which contains
    the required data

    Input:
    ------
    data : key-value object
    layout : nested object
    """
    validate_layout(layout)

    dataframes = dataframe
    if type(dataframes) not in [tuple, list]:
        dataframes = [dataframes]

    available_varnames = _get_available_varnames(dataframes)

    layout = preprocess_fnc(layout, available_varnames)

    display_params = _initialize_display(layout)
    max_graphs = display_params['max_graphs']
    fig_layout = display_params['layout']

    figure_number = 0
    graph_position = 1
    for graph_layout in layout['graphs']:
        if (graph_position > max_graphs or figure_number == 0):
            graph_position = 1
            figure_number += 1
            figure = _build_new_figure(fig_number=figure_number, **display_params)

        subplot = figure.add_subplot(*fig_layout, graph_position)
        _build_new_graph(subplot, graph_layout, dataframes)
        figure.tight_layout()
        graph_position += 1

    if show:
        plt.show()


def _get_available_varnames(dataframes):
    var_names = set(dataframes[0].keys())
    for dataframe in dataframes[1:]:
        var_names = var_names.intersection(set(dataframe.keys()))

    return list(var_names)


def _initialize_display(layout):
    """Sets some parameters needed by the function "display", thanks
    to the provided layout, and returns them in a dictionary.
    """

    n_graphs = len(layout['graphs'])

    fig_layout = layout.get('figure_structure', None)
    if fig_layout is None:
        fig_layout = [1, n_graphs] if n_graphs < 4 else [2, 3]

    n_max_graphs_fig = fig_layout[0] * fig_layout[1]
    n_figs = math.ceil(n_graphs / n_max_graphs_fig)

    display_params = {
        'layout': fig_layout,
        'max_graphs': n_max_graphs_fig,
        'n_figs': n_figs,
        'dpi': layout.get('figure_dpi', None),
        'inches_size': layout.get('figure_size', (14, 8)),
        'title': layout.get('title', '')}

    return display_params


def _build_new_figure(dpi=None, inches_size=(14, 8), fig_number=None,
                      n_figs=None, title="", **kwargs):
    figure = plt.figure(dpi=dpi)
    figure.set_size_inches(*inches_size)

    figure_name = title
    if fig_number is not None and n_figs is not None and n_figs > 1:
        figure_name += f' ({fig_number}/{n_figs})'

    figure.canvas.manager.set_window_title(figure_name.strip())

    figure.subplots_adjust(left=0.11, bottom=0.12,
                           right=0.9, top=0.9,
                           wspace=0.38, hspace=0.32)

    return figure


def _build_new_graph(ax, graph_layout, dataframes):
    """Builds a new matplotlib graph in the provided subplot.
    """
    # TODO: handle tick label overlap
    # TODO: handle no legend space

    ax.set_xlabel(graph_layout.get('x_label', None))
    ax.set_ylabel(graph_layout.get('y_label', None))
    ax.set_title(graph_layout.get('title', None), fontweight='bold')

    for data_number, dataframe in enumerate(dataframes):
        x = dataframe[graph_layout["x_var"]]
        for curve_number, curve_layout in enumerate(graph_layout["curves"]):
            symbols_markevery = _symbols_markevery(x)
            custom_style = _custom_cycler_iterate(curve_number, data_number, symbols_markevery)
            ax.plot(x, dataframe[curve_layout["var"]], **custom_style)

    # improve legends
    handles = []
    for curve_number, curve_layout in enumerate(graph_layout['curves']):
        label = curve_layout.get('legend', None)
        custom_style = _custom_cycler_iterate(curve_number, 0)

        if label is not None:
            handles.append(Line2D([0], [0], label=label, **custom_style))

    n_data = len(dataframes)
    if n_data > 1:
        for i in range(n_data):
            label = f'data {i}'
            custom_style = _custom_cycler_iterate(0, i)
            custom_style['color'] = 'black'
            handles.append(Line2D([0], [0], label=label, **custom_style))

    if handles:
        ax.legend(handles=handles, loc=0)


def _custom_cycler_iterate(index, family_index=0, markevery=None):
    """Returns a dict with keys and values in accordance to the Matplotlib
    syntax.

    The graph style includes a color from a list defined for colour-blind people
    by https://personal.sron.nl/~pault/, depending if the data is compared with
    other data.
    """

    custom_cycler = ['#4477AA', '#66CCEE', '#228833', '#CCBB44', '#EE6677',
                     '#AA3377', '#BBBBBB']
    custom_linestyles = ['solid', 'dashed', 'dotted', 'dash']
    custom_symbols = ["d", "o", "v", "^", ">", "<", "s"]

    index_ = index % len(custom_cycler)
    family_index_ = family_index % len(custom_linestyles)

    custom_style = {
        'color': custom_cycler[index_],
        'linestyle': custom_linestyles[family_index_],
        'marker': custom_symbols[index_],
        'markevery': markevery,
    }

    if index > len(custom_cycler):
        warnings.warn(f"More than {len(custom_cycler)} plots on one graph!")

    if family_index > len(custom_linestyles):
        warnings.warn(f"More than {len(custom_linestyles)} types of curves on one graph")

    return custom_style

def _symbols_markevery(x):
    """compute indices where to show symbols

    Args:
        x (numpy array): x vector
    """
    nb_markers = 10
    # set step between two symbols
    step = floor(len(x) / (nb_markers-1))
    # list of symbols indexes (first symbol at first index)
    symbols_indexes = [k * step for k in range(nb_markers)]

    return symbols_indexes