import matplotlib.pyplot as plt
import numpy as np

import scipy.stats as stats
from itertools import zip_longest

def phase_comparison_figure(
        measured_phases,
        true_phases,
        plot_residuals=True,
        f_beacon=None,
        hist_kwargs={},
        sc_kwargs={},
        text_kwargs={},
        colors=['blue', 'orange'],
        legend_on_scatter=True,
        secondary_axis='time',
        fit_gaussian=False,
        mean_snr=None,
        return_fit_info=False,
        **fig_kwargs
        ):
    """
    Create a figure comparing measured_phase against true_phase
    by both plotting the values, and the residuals.
    """
    default_fig_kwargs = dict(sharex=True)
    default_hist_kwargs = dict(bins='sqrt', density=False, alpha=0.8, histtype='step')
    default_text_kwargs = dict(fontsize=14, verticalalignment='top')
    default_sc_kwargs = dict(alpha=0.6, ls='none')

    fig_kwargs = {**default_fig_kwargs, **fig_kwargs}
    hist_kwargs = {**default_hist_kwargs, **hist_kwargs}
    text_kwargs = {**default_text_kwargs, **text_kwargs}
    sc_kwargs = {**default_sc_kwargs, **sc_kwargs}

    do_hist_plot = hist_kwargs is not False
    do_scatter_plot = sc_kwargs is not False

    fig, axs = plt.subplots(0+do_hist_plot+do_scatter_plot, 1, **fig_kwargs)

    if not hasattr(axs, '__len__'):
        axs = [axs]

    if f_beacon and secondary_axis in ['phase', 'time']:
        phase2time = lambda x: x/(2*np.pi*f_beacon)
        time2phase = lambda x: 2*np.pi*x*f_beacon

        if secondary_axis == 'time':
            functions = (phase2time, time2phase)
            label = 'Time $\\varphi/(2\\pi f_{beac})$ [ns]'
        else:
            functions = (time2phase, phase2time)
            label = 'Phase $2\\pi t f_{beac}$ [rad]'

        secax = axs[0].secondary_xaxis('top', functions=functions)

    # Histogram
    fit_info = {}
    if do_hist_plot:
        i=0

        axs[i].set_ylabel("#")

        this_kwargs = dict(
                ax = axs[i],
                text_kwargs=text_kwargs,
                hist_kwargs={**hist_kwargs, **dict(label='Measured', color=colors[0], ls='solid')},
                mean_snr=mean_snr,
                )

        if fit_gaussian:
            this_kwargs['fit_distr'] = 'gaussian'

        _, fit_info = fitted_histogram_figure(
                measured_phases,
                **this_kwargs
                )

        if not plot_residuals: # also plot the true clock phases
            _bins = fit_info['bins']
            axs[i].hist(true_phases, color=colors[1], label='Actual', ls='dashed', **{**hist_kwargs, **dict(bins=_bins)})

    # Scatter plot
    if do_scatter_plot:
        i=1
        axs[i].set_ylabel("Antenna no.")
        axs[i].plot(measured_phases, np.arange(len(measured_phases)), marker='x' if plot_residuals else '3', color=colors[0], label='Measured', **sc_kwargs)

        if not plot_residuals: # also plot the true clock phases
            axs[i].plot(true_phases, np.arange(len(true_phases)), marker='4', color=colors[1], label='Actual', **sc_kwargs)

        if not plot_residuals and legend_on_scatter:
            axs[i].legend()

    fig.tight_layout()

    if return_fit_info:
        return fig, fit_info

    return fig


def fitted_histogram_figure(
        amplitudes,
        fit_distr = None,
        calc_chisq = True,
        text_kwargs={},
        hist_kwargs={},
        mean_snr = None,
        ax = None,
        **fig_kwargs
    ):
    """
    Create a figure showing $amplitudes$ as a histogram.
    If fit_distr is a (list of) string, also fit the respective
    distribution function and show the parameters on the figure.
    """
    default_hist_kwargs = dict(bins='sqrt', density=False, alpha=0.8, histtype='step', label='hist')
    default_text_kwargs = dict(fontsize=14, verticalalignment='top')

    if isinstance(fit_distr, str):
        fit_distr = [fit_distr]

    hist_kwargs = {**default_hist_kwargs, **hist_kwargs}
    text_kwargs = {**default_text_kwargs, **text_kwargs}

    if ax is None:
        fig, ax = plt.subplots(1,1, **fig_kwargs)
    else:
        fig = ax.get_figure()

    text_kwargs['transform'] = ax.transAxes

    counts, bins, _patches = ax.hist(amplitudes, **hist_kwargs)

    fit_info = []
    if fit_distr:
        min_x = min(amplitudes)
        max_x = max(amplitudes)

        dx = bins[1] - bins[0]
        scale = len(amplitudes) * dx

        xs = np.linspace(min_x, max_x)

        for distr in fit_distr:
            fit_params2text_params = lambda x: x

            if 'rice' == distr:
                name = "Rice"
                param_names = [ "$\\nu$", "$\\sigma$" ]
                distr_func = stats.rice

                fit_params2text_params = lambda x: (x[0]*x[1], x[1])

            elif 'gaussian' == distr:
                name = "Norm"
                param_names = [ "$\\mu$", "$\\sigma$" ]
                distr_func = stats.norm

            elif 'rayleigh' == distr:
                name = "Rayleigh"
                param_names = [ "$\\sigma$" ]
                distr_func = stats.rayleigh

                fit_params2text_params = lambda x: (x[0]+x[1]/2,)

            else:
                raise ValueError('Unknown distribution function '+distr)

            label = name +"(" + ','.join(param_names) + ')'

            fit_params = distr_func.fit(amplitudes)
            fit_ys = distr_func.pdf(xs, *fit_params)

            ax.plot(xs, fit_ys*scale, label=label)

            chisq_strs = []
            if calc_chisq:
                ct = np.diff(distr_func.cdf(bins, *fit_params))*np.sum(counts)
                c2t = stats.chisquare(counts, ct, ddof=len(fit_params))
                chisq_strs = [
                        f"$\\chi^2$/dof = {c2t[0]: .2g}/{len(fit_params)}"
                        ]

            # change parameters if needed
            text_fit_params = fit_params2text_params(fit_params)

            text_str = "\n".join(
                [label]
                +
                [ f"{param} = {value: .2e}" for param, value in zip_longest(param_names, text_fit_params, fillvalue='?') ]
                +
                chisq_strs
                )

            this_info = {
                    'name': name,
                    'param_names': param_names,
                    'param_values': text_fit_params,
                    'text_str': text_str,
                }

            if chisq_strs:
                this_info['chisq'] = c2t[0]
                this_info['dof'] = len(fit_params)

            fit_info.append(this_info)

        loc = (0.02, 0.95)
        ax.text(*loc, "\n\n".join([info['text_str'] for info in fit_info]), **{**text_kwargs, **dict(ha='left')})

    if mean_snr:
        text_str = f"$\\langle SNR \\rangle$ = {mean_snr: .1e}"
        loc = (0.98, 0.95)
        ax.text(*loc, text_str, **{**text_kwargs, **dict(ha='right')})

    return fig, dict(fit_info=fit_info, counts=counts, bins=bins)
