#!/usr/bin/env python3
# -*- coding: utf-8 -*-
""" Utils module with utility functions """

import math
import os.path
import xarray as xr
import numpy as np
import pandas as pd
import feather
import logging
from dash import dcc
from dash import html
from map_handler import PATHNAME
from map_handler import RENDER404

def concat_dataframes(fname_tpl, months, variable, rename_from=None, notnans=None):
    """ Concatenate monthly dataframes

    Parameters
    ----------
    fname_tpl : str
        Feather file path
    months : numpy.ndarray
        Months, e.g. ['202304', '202305']
    variable : str
        Variable name
    rename_from : str, optional
        Variable name after renaming it, by default None
    notnans : list, optional
        Stations that have only NaN values, by default None

    Returns
    -------
    list
        Stations that have only NaN values
    pandas.core.frame.DataFrame
        Dataframe with data from all months
    """

    # Build feather files paths
    opaths = [fname_tpl.format(month, variable)
        for month in months if os.path.exists(fname_tpl.format(month, variable))]

    logging.debug("__________ %s __________", opaths)
    if not opaths:
        return None, None

    # Read monthly dataframes and concatenate into one
    if rename_from:
        mon_dfs = pd.concat([feather.read_dataframe(opath)
            .rename(columns={rename_from: variable})
            for opath in opaths])
    # In case of models we don't rename the variable column
    else:
        mon_dfs = pd.concat([feather.read_dataframe(opath)
            for opath in opaths])

    # 1d observations
    if 'station' in mon_dfs.columns:
        if notnans is None:
            notnans = [st for st in mon_dfs['station'].unique()
                if not mon_dfs[mon_dfs['station']==st][variable].isnull().all()]
        mon_dfs_filter = mon_dfs['station']
        final_df = mon_dfs[mon_dfs_filter.isin(notnans)]
    # 2d observations
    else:
        final_df = mon_dfs

    return notnans, final_df


def retrieve_single_point(fname, tstep, lat, lon, variable):
    """ Retrive data on single point

    Parameters
    ----------
    fname : str
        File path
    tstep : int
        Timestep
    lat : float
        Latitude
    lon : float
        Longitude
    variable : str
        Variable name

    Returns
    -------
    float
        Variable value at closest available point from (lat, lon)
    """

    from map_handler import DEBUG

    logging.debug(" %s %s %s %s %s", fname, tstep, lat, lon, variable)
    ds = xr.open_dataset(fname)
    if variable not in ds.variables:
        variable = variable.lower()

    # logging.debug('TIMESERIES %s %s %s %s', fname, variable, lon, lat)

    if 'lat' in ds.variables:
        da = ds[variable].sel(lon=lon, lat=lat, method='nearest')
    else:
        da = ds[variable].sel(longitude=lon, latitude=lat, method='nearest')
    logging.debug('%s', da)

    return da.values[tstep]


def retrieve_timeseries(fname, lat, lon, variable, method='netcdf', forecast=False):
    """ Retrieve timeseries data

    Parameters
    ----------
    fname : str
        File path
    lat : float
        Latitude
    lon : float
        Longitude
    variable : str
        Variable name
    method : str, optional
        Method name, by default 'netcdf'
    forecast : bool, optional
        Indicates if we are using forecast data, by default False

    Returns
    -------
    numpy.ndarray
        Latitudes
    numpy.ndarray
        Longitudes
    pandas.core.indexes.datetimes.DatetimeIndex
        Times
    xarray.core.dataarray.DataArray
        Data
    """

    from map_handler import DEBUG

    if method == 'feather' and not forecast:
        df = feather.read_dataframe(fname)
        if 'lat' in df.columns:
            lat_col = 'lat'
            lon_col = 'lon'
        else:
            lat_col = 'latitude'
            lon_col = 'longitude'

        if variable not in df.columns:
            variable = variable.lower()

        if variable not in df.columns:
            return None, None, None, None

        n_lon = find_nearest(df[lon_col].values, lon)
        n_lat = find_nearest(df[lat_col].values, lat)
        ts = df[(df[lat_col] == n_lat) & (df[lon_col] == n_lon)][['time',
            variable]].set_index('time')

        return n_lat, n_lon, ts.index, ts[variable]

    def preprocess(ds, n=8):
        return ds.isel(time=range(n))

    if forecast:
        ds = xr.open_dataset(fname)
    else:
        ds = xr.open_mfdataset(fname, concat_dim='time', combine='nested',
                preprocess=preprocess)
    if variable not in ds.variables:
        variable = variable.lower()

    # logging.debug('TIMESERIES %s %s %s %s', fname, variable, lon, lat)

    if 'lat' in ds.variables:
        da = ds[variable].sel(lon=lon, lat=lat, method='nearest')
        clat = 'lat'
        clon = 'lon'
    else:
        da = ds[variable].sel(longitude=lon, latitude=lat, method='nearest')
        clat = 'latitude'
        clon = 'longitude'
    
    return da[clat].values, da[clon].values, da.indexes['time'], da


def find_nearest(array, value):
    """ Find the nearest value of a couple of coordinates

    Parameters
    ----------
    array : numpy.ndarray
        Array from which we extract the closest value (e.g. latitudes array)
    value : float
        Value for which we will search its closest value

    Returns
    -------
    float
        Nearest value in array for value
    """
    
    return array[np.abs(array-value).argmin()]


def find_nearest2(array, value):
    """ Find the nearest value of a couple of coordinates

    Parameters
    ----------
    array : numpy.ndarray
        Array from which we extract the closest value (e.g. latitudes array)
    value : float
        Value for which we will search its closest value

    Returns
    -------
    float
        Nearest value in array for value
    """

    idx = np.searchsorted(array, value, side="left")
    if idx > 0 and (idx == len(array) or math.fabs(value - array[idx-1]) <
                    math.fabs(value - array[idx])):
        return array[idx-1]

    return array[idx]


def calc_matrix(n):
    """ Calculate the mosaic optimum matrix shape

    Parameters
    ----------
    n : int
        Number of mosaic cells

    Returns
    -------
    int
        Number of columns
    int
        Number of rows
    """

    sqrt_n = math.sqrt(n)
    ncols = sqrt_n == int(sqrt_n) and int(sqrt_n) or int(sqrt_n) + 1
    nrows = n%ncols > 0 and int(n/ncols)+1 or int(n/ncols)
    
    return ncols, nrows


def magnitude(num):
    """ Calculate magnitude

    Parameters
    ----------
    num : float
        Number

    Returns
    -------
    int
        Magnitude of num
    """

    if num == 0:
        num = 1

    return int(math.floor(math.log10(num)))


def normalize_vals(vals, valsmin, valsmax, rnd=2):
    """ Normalize values to 0-1 scale

    Parameters
    ----------
    vals : numpy.ndarray
        Values to normalize
    valsmin : float
        Minimum
    valsmax : float
        Maximum
    rnd : int, optional
        Rounding decimals, by default 2

    Returns
    -------
    numpy.ndarray
        Normalized values
    """

    if len(vals) == 1:
        return np.array([0])
    vals = np.array(vals)
    if rnd < 2:
        rnd = 2

    return np.around((vals-valsmin)/(valsmax-valsmin), rnd)


def render404():
    """ Create a 404 page """

    # Define error message
    children = [html.H2('404 Error', id='error_title'),
                html.P("Sorry we can't find the page you were looking for."),
                html.P("Here are some helpful links that might help:"),]
    
    # Define links
    for tab_i, tab in enumerate(RENDER404.keys()):
        # Add spacing if between links, not above
        if tab_i > 0:
            children.extend([html.Br(),
                             html.Br(),])
        # Add links to tabs
        children.extend([dcc.Link(RENDER404[tab]['name'], 
                                  id=RENDER404[tab]['id'],
                                  href=PATHNAME+RENDER404[tab]['path'],
                                  className='error_links',target='_parent',
                                  refresh=True),])

    # Create page
    page = [html.Div(
            className='background',
            children=[
                html.Div(
                    id='error_div',
                    children=children,
                    )
                ]
            )
            ]

    return page


def extend_list(l):
    """ Extend list of lists to a flat list

    Parameters
    ----------
    l : list
        List

    Returns
    -------
    list
        Flat list
    """

    res = []
    for x in l:
        res.extend(x)
    if not isinstance(res, list):
        res = [res]
        
    return res