# -*- coding: utf-8 -*-
""" Data Handler """

from dash import html
import dash_leaflet as dl
import dash_leaflet.express as dlx
from dash_extensions.javascript import arrow_function
from dash_extensions.javascript import Namespace
import matplotlib
from matplotlib import cm
from matplotlib.colors import ListedColormap
import numpy as np
from netCDF4 import Dataset as nc_file
import pandas as pd
import json
import orjson
from collections import OrderedDict
import os
import time
import calendar
from datetime import timedelta
from datetime import datetime as dt
from dateutil.relativedelta import relativedelta

from ines_core_map_handler import MapHandler
from ines_core_map_handler import PointsFigureHandler
from ines_core_map_handler import ContourFigureHandler
from ines_core_map_handler import ShapefileFigureHandler

from pathlib import Path
from flask_caching import Cache
import uuid
import socket
import logging

DIR_PATH = os.path.dirname(os.path.realpath(__file__))
DEBUG = False
DASH_LOG_LEVEL = logging.INFO # DEBUG INFO WARNING ERROR

# Set up cache
CACHE = json.load(open(os.path.join(DIR_PATH, 'conf/cache.json')))
cache_config = CACHE['config']
cache_config.update({'CACHE_DIR': "/dev/shm/{}".format(str(uuid.uuid1()))})
Path(cache_config['CACHE_DIR']).mkdir(parents=True, exist_ok=True)
cache = Cache(config=cache_config)
cache_timeout = CACHE['timeout']

# Set up base url
HOSTNAME = socket.gethostbyname_ex(socket.gethostname())[0]
HOSTNAMES = json.load(open(os.path.join(DIR_PATH, 'conf/hostnames.json')))

# Get configurations
INIT = json.load(open(os.path.join(DIR_PATH, 'conf/init.json')))
DASH_STYLE = json.load(open(os.path.join(DIR_PATH, 'conf/dash_style.json')))
VARS = json.load(open(os.path.join(DIR_PATH, 'conf/vars.json')))
MODELS = json.load(open(os.path.join(DIR_PATH, 'conf/models.json')))
OBS = json.load(open(os.path.join(DIR_PATH, 'conf/obs.json')))
WAS = json.load(open(os.path.join(DIR_PATH, 'conf/was.json')))
PROB = json.load(open(os.path.join(DIR_PATH, 'conf/prob.json')))
VIS = json.load(open(os.path.join(DIR_PATH, 'conf/vis.json')))
SCORES = json.load(open(os.path.join(DIR_PATH, 'conf/scores.json')))
DATES = json.load(open(os.path.join(DIR_PATH, 'conf/dates.json')))
ALIASES = json.load(open(os.path.join(DIR_PATH, 'conf/aliases.json')))
STATS_CONF = json.load(open(os.path.join(DIR_PATH, 'conf/stats.json')))
ALL_COLORS = json.load(open(os.path.join(DIR_PATH, 'conf/colors.json')))
MODEBARS = json.load(open(os.path.join(DIR_PATH, 'conf/modebars.json')))
DISCLAIMERS = json.load(open(os.path.join(DIR_PATH, 'conf/disclaimers.json')))
SATELLITE_IMAGE_SRC = json.load(open(os.path.join(DIR_PATH, 'conf/satellite_image_src.json')))
PATHS = json.load(open(os.path.join(DIR_PATH, 'conf/paths.json')))
RENDER404 = json.load(open(os.path.join(DIR_PATH, 'conf/render404.json')))

# Set up initial conditions
FREQ = INIT['frequency']
FORECAST_MAX = INIT['forecast_max']
FORECAST_FINAL_DAY = INIT['forecast_final_day']
DEFAULT_VAR = INIT['default_variable']
DEFAULT_MODEL = INIT['default_model']
START_DATE = DATES['start_date']
DELAY = DATES['delay']['delayed']
DELAY_DATE = DATES['delay']['start_date']
END_DATE = DATES['end_date']
END_DATE = END_DATE or (DELAY and (dt.now() -
    timedelta(days=1)).strftime("%Y%m%d") or dt.now().strftime("%Y%m%d"))

# Set up dashboard basic properties
GRAPH_HEIGHT = DASH_STYLE['graph_height']
OPACITY = DASH_STYLE['opacity']

# Set up color scheme
COLORS = ALL_COLORS['std']
COLORS_NORGB = ALL_COLORS['std_norgb']
COLORS_PROB = ALL_COLORS['prob']
COLORMAP = ListedColormap(COLORS_NORGB)
COLORMAP_PROB = ListedColormap(COLORS_PROB)

# Set up route defaults
ROUTE_DEFAULTS = json.load(open(os.path.join(DIR_PATH, 'conf/route.json')))
ROUTE_DEFAULTS.update({'date': [END_DATE]})

# Set up statistics
STATS = OrderedDict([(key, STATS_CONF[key]['name']) for key in STATS_CONF])
STATS.update({'totn': 'TOTAL CASES'})

# Set up timeseries configuration
MODEBAR_CONFIG = MODEBARS['config']
MODEBAR_CONFIG_TS = MODEBARS['config_ts']
MODEBAR_LAYOUT = MODEBARS['layout']
MODEBAR_LAYOUT_TS = MODEBARS['layout_ts']

# Set up disclaimers
DISCLAIMER_MODELS = [html.Span(html.P(DISCLAIMERS['models']), id='models-disclaimer')]
DISCLAIMER_OBS = html.P(DISCLAIMERS['observations'])

# Get templates
GEOJSON_TEMPLATE = "{}/geojson/{}/{:02d}_{}_{}.geojson"
NETCDF_TEMPLATE = "{}/netcdf/{}{}.nc"

# Get path names
if HOSTNAME in HOSTNAMES:
    PATHNAME = HOSTNAMES[HOSTNAME]
else:
    PATHNAME = HOSTNAMES['default']


class ForecastModelsFigureHandler(ContourFigureHandler):
    """ Class which handles forecast model data """

    def __init__(self, var=None, model=None, tstep=0, hour=None, selected_date=None):
        """ Initialize ForecastModelsFigureHandler class

        Parameters
        ----------
        var : str, optional
            Variable name, by default None
        model : str, optional
            Model name, by default None
        tstep : int, optional
            Timestep, by default 0
        hour : int, optional
            Hour, by default None
        selected_date : str, optional
            Selected date, by default None
        """

        if isinstance(model, list):
            self.model = model[0]
        else:
            self.model = model
        self.name = self.model
        super(ForecastModelsFigureHandler, self).__init__()

        self.var = var
        self.selected_date = selected_date

        self.bounds = np.array(VARS[self.var.upper()]['bounds']).astype('float32')
        self.style = dict(weight=0, opacity=0, color='white', dashArray='', fillOpacity=OPACITY)
        self.colorscale = COLORS

        if self.selected_date:
            # Get NetCDF file path
            self.filepath = NETCDF_TEMPLATE.format(MODELS[self.model]['path'], selected_date, 
                                                   MODELS[self.model]['template'])
            
            # Read NetCDF file and retrieve timesteps, what (hours, months, etc.) and rdatetime
            if os.path.exists(self.filepath):
                self.timesteps, self.what, self.rdatetime = self.get_time_details()

        return None

    def generate_var_tstep_trace(self, tstep=0):
        """ Generate GeoJSON trace to be added per timestep

        Parameters
        ----------
        tstep : int, optional
            Timestep, by default 0
        """

        data_path = os.path.basename(MODELS[self.model]['path'])
        geojson_path = os.path.join('geojsons', GEOJSON_TEMPLATE.format(data_path, 
            self.selected_date, tstep, self.selected_date, self.var))
        namespace = Namespace("forecastTab", "forecastMaps")
        self.geojson = self.retrieve_geojson(geojson_path, namespace)

        return None

    def generate_colorbar(self):
        """ Generate colorbar """
        
        ctg = ["{:d}".format(int(cls)) if cls.as_integer_ratio()[1] == 1 else "{:.1f}".format(cls) 
               for i, cls in enumerate(self.bounds[1:-1])]
        indices = list(range(len(ctg) + 2))
        self.colorbar_info.update({'min': 0,
                                   'max': len(ctg)+1, 
                                   'classes': indices,
                                   'colorscale': self.colorscale,
                                   'tickValues': indices[1:-1],
                                   'tickText': ctg})
        self.colorbar = self.retrieve_colorbar()

        logging.debug("BOUNDS %s", self.bounds)
        logging.debug("CTG %s", ctg)

        return None

    def get_title(self, **kwargs):
        """ Return title from base title and elements """

        varname = kwargs['varname']
        tstep = kwargs['tstep']

        self.base_title = " ".join([MODELS[self.model]['name'], VARS[varname]['title']])
        self.cdatetime = self.retrieve_cdatetime(tstep)
        self.step = "{:02d}".format(tstep*FREQ)
        title = super(ForecastModelsFigureHandler, self).get_title()
        
        return title

    def get_figure_layers(self, tstep=0, hour=None, aspect=(1,1)):
        """ Get figure layers (GeoJSON trace, colorbar and information box with title)

        Parameters
        ----------
        tstep : int, optional
            Timestep, by default 0
        hour : int, optional
            Hour, by default None
        aspect : tuple, optional
            Map aspect, by default (1,1)

        Returns
        -------
        list
            Figure layers
        """

        logging.debug('Updating layout ...')
        
        if os.path.exists(self.filepath) and self.var:
            
            # Get timestep
            if hour is not None:
                tstep = int(self.hour_to_tstep(hour))
            else:
                tstep = int(tstep)

            # Get trace and colorbar
            self.generate_var_tstep_trace(tstep)
            self.generate_colorbar()

            # Get figure title
            self.fig_title = html.P(html.B(
                [
                    item for sublist in self.get_title(varname=self.var, tstep=tstep).split('<br>') 
                    for item in [sublist, html.Br()]
                ][:-1]
            ))

        else:
            self.geojson = None
            self.colorbar = None

            # Get figure title
            if not self.var:
                self.fig_title = ""
            elif self.var and not os.path.exists(self.filepath):
                if self.model in MODELS:
                    title = "{} - DATA NOT AVAILABLE".format(MODELS[self.model]['name'])
                else:
                    title = "DATA NOT AVAILABLE"
                self.fig_title = html.P(html.B(title))

        if self.model is not None:
            # Tilt colorbar values so they won't overlap for SCONC_DUST
            if self.var == "SCONC_DUST": 
                self.colorbar.className = 'sconc_info'
            if aspect[0] > 2:
                self.info_style['fontSize'] = "{}px".format(int(self.info_style['fontSize'][:-2])-aspect[0]+ 0.3)
        
        # Get title information element
        self.info = self.retrieve_info(name=self.model)

        return [self.geojson, self.colorbar, self.info]


class ForecastProbFigureHandler(ContourFigureHandler):
    """ Class which handles forecast probability data """

    def __init__(self, var=None, prob=None, selected_date=None):
        """ Initialize ForecastProbFigureHandler class

        Parameters
        ----------
        var : str, optional
            Variable name, by default None
        prob : str, optional
            Probability threshold, by default None
        selected_date : str, optional
            Selected date, by default None
        """
        
        self.name = 'prob'
        super(ForecastProbFigureHandler, self).__init__()

        if var is None:
            self.var = DEFAULT_VAR
        else:
            self.var = var

        probs = PROB[self.var]['prob_thresh']
        if prob is None:
            prob = probs[0]

        self.prob = prob
        self.selected_date = selected_date

        self.bounds = np.arange(0, 110, 10)
        self.colorscale = COLORS_PROB
        self.style = dict(weight=0, opacity=0, color='white', dashArray='', fillOpacity=OPACITY)

        # Get NetCDF file path
        netcdf_path = PROB[self.var]['netcdf_path']
        netcdf_file = PROB[self.var]['netcdf_template']
        self.filepath = os.path.join(netcdf_path, netcdf_file).format(prob=self.prob, 
                                                                      date=self.selected_date, 
                                                                      var=self.var)

        # Read NetCDF file and retrieve timesteps, what (hours, months, etc.) and rdatetime
        if os.path.exists(self.filepath):
            self.timesteps, self.what, self.rdatetime = self.get_time_details()

        return None
        
    def generate_var_tstep_trace(self, tstep=0):
        """ Generate GeoJSON trace to be added per timestep

        Parameters
        ----------
        tstep : int, optional
            Timestep, by default 0
        """

        geojson_path = os.path.join(PROB[self.var]['geojson_path'], 
                                    PROB[self.var]['geojson_template']).format(prob=self.prob, 
                                        date=self.selected_date, var=self.var)
        geojson_path = geojson_path.format(step=tstep).replace('/data/daily_dashboard', 
                                                               'geojsons')
        namespace = Namespace("forecastTab", "forecastMaps") 
        self.geojson = self.retrieve_geojson(geojson_path, namespace)

        return None

    def generate_colorbar(self):
        """ Generate colorbar """

        ctg = ["{:.1f}".format(cls) if '.' in str(cls) else "{:d}".format(cls)
                for i, cls in enumerate(self.bounds)]
        indices = list(range(len(ctg)))
        self.colorbar_info.update({'min': -0.1,
                                   'max': len(ctg)-.7,
                                   'classes': indices,
                                   'colorscale': self.colorscale,
                                   'tickValues': indices,
                                   'tickText': ctg})
        self.colorbar = self.retrieve_colorbar()

        return None

    def get_title(self, **kwargs):
        """ Return title from base title and elements """
        
        tstep = kwargs['tstep']

        self.members = 0
        for model in MODELS:
            if model == 'median':
                continue
            path_template = '{}{}.nc'.format(self.rdatetime.strftime("%Y%m%d"), MODELS[model]['template'])
            fpath = os.path.join(MODELS[model]['path'], 'netcdf', path_template)
            if os.path.exists(fpath):
                self.members += 1

        self.base_title = PROB[self.var]['title']
        self.cdatetime = self.rdatetime + relativedelta(days=tstep)

        title = super(ForecastProbFigureHandler, self).get_title()

        return title

    def get_figure_layers(self, day=0):
        """ Get figure layers (GeoJSON trace, colorbar and information box with title)

        Parameters
        ----------
        day : int, optional
            Day, by default 0

        Returns
        -------
        list
            Figure layers
        """
        
        logging.debug('Updating layout ...')

        if self.var and os.path.exists(self.filepath):
            
            # Get timestep
            tstep = int(day)

            # Get trace and colorbar
            self.generate_colorbar()
            self.generate_var_tstep_trace(tstep)

            # Get figure title
            self.fig_title = html.P(html.B(
                [
                    item for sublist in self.get_title(varname=self.var, tstep=tstep).split('<br>') 
                    for item in [sublist, html.Br()]
                ][:-1]
            ))
        else:
            self.geojson = None
            self.colorbar = None

            # Get figure title
            if not self.var:
                self.fig_title = ""
            else:
                self.fig_title = html.P(html.B("DATA NOT AVAILABLE"))

        # Get title information element
        self.info = self.retrieve_info(self.name)
        
        return [self.geojson, self.colorbar, self.info]


class ForecastWasFigureHandler(ShapefileFigureHandler):
    """ Class which handles forecast WAS data """

    def __init__(self, was='burkinafaso', model='median', var='SCONC_DUST', selected_date=None):
        """ Initialize ForecastWasFigureHandler class

        Parameters
        ----------
        was : str, optional
            Region name, by default 'burkinafaso'
        model : str, optional
            Model name, by default 'median'
        var : str, optional
            Variable name, by default 'SCONC_DUST'
        selected_date : str, optional
            Selected date, by default None
        """
        
        self.name = 'was'
        super(ForecastWasFigureHandler, self).__init__()

        self.model = model
        self.was = was
        self.var = var
        self.selected_date = selected_date

        self.colormap = WAS[self.was]['colormap']
        self.hideout_style = WAS[self.was]['hideout_style']
        self.hideout_style.update({'fillOpacity': OPACITY})
        self.hover_style = WAS[self.was]['hover_style']
        self.hover_style.update({'fillOpacity': OPACITY})

        if (self.model in MODELS) and self.selected_date:
            
            # Get NetCDF file path
            self.filepath = NETCDF_TEMPLATE.format(
                MODELS[self.model]['path'],
                selected_date,
                MODELS[self.model]['template']
            )
            
            if os.path.exists(self.filepath):
                # Read NetCDF file and retrieve timesteps, what (hours, months, etc.) and rdatetime
                self.timesteps, self.what, self.rdatetime = self.get_time_details()
            
                # Correct timesteps to show first timestep of each day ([0, 24, 48, ...])
                self.timesteps = self.timesteps[::8]

        return None

    def get_regions_data(self, day=0):
        """ Get regions data

        Parameters
        ----------
        day : int, optional
            Day, by default 0

        Returns
        -------
        numpy.ndarray
            Region names
        numpy.ndarray
            Region colors
        numpy.ndarray
            Region definitions
        """

        input_dir = WAS[self.was]['path'].format(was=self.was, format='h5', date=self.selected_date)
        input_file = WAS[self.was]['template'].format(date=self.selected_date, var=self.var, 
                                                      format='h5')

        input_path = os.path.join(input_dir, input_file)

        logging.debug("INPUT PATH %s %s", input_path, self.selected_date)
        if not os.path.exists(input_path):
            return [], [], []

        df = pd.read_hdf(input_path, 'was_{}'.format(self.selected_date)).set_index('day')

        names, colors, definitions = df.loc['Day{}'.format(day)].values.T
        
        return names, colors, definitions

    def get_geojson_url(self, day=0):
        """ Get GeoJSON url

        Parameters
        ----------
        day : int, optional
            Day, by default 0

        Returns
        -------
        str
            Path to GeoJSON data
        """

        geojsons_dir = WAS[self.was]['path'].format(was=self.was, format='geojson', 
                                                    date=self.selected_date)
        geojson_file = WAS[self.was]['template'].format(date=self.selected_date, var=self.var, 
                                                        format='geojson').replace('.geojson', 
                                                        '_{}.geojson'.format(day))
        geojson_filepath = os.path.join(geojsons_dir, geojson_file)
        if not os.path.exists(geojson_filepath):
            logging.debug("ERROR: WAS GEOJSON URL %s", geojson_filepath)
            return None

        pathlist = os.path.normpath(geojsons_dir).split(os.sep)
        geojsons_dir_cleaned = os.sep.join(pathlist[pathlist.index('was'):])

        geojson_path = os.path.join(geojsons_dir_cleaned, geojson_file)
        geojson_url = os.path.join('geojsons', geojson_path)

        logging.debug("WAS GEOJSON URL %s", geojson_url)

        return geojson_url

    def generate_var_tstep_trace(self, day=0):
        """ Generate GeoJSON trace to be added per day

        Parameters
        ----------
        day : int, optional
            Day, by default 0
        """

        logging.debug('Adding contours ...')

        day = int(day)
        names, colors, definitions = self.get_regions_data(day=day)
        colors = np.array(colors)
        logging.debug(":::::::::: %s %s %s", names, colors, definitions)

        # Create legend
        self.legend = self.create_legend(self.colormap)

        # Geojson rendering logic, must be JavaScript as it is executed in clientside.
        namespace = Namespace("forecastTab", "wasMaps")
        geojson_path = self.get_geojson_url(day=day)
        hideout = dict(colorscale=list(self.colormap.values()),
                       bounds=[i for i in range(len(self.colormap.values()))],
                       style=self.hideout_style,
                       colorProp="value")
        hover_style = arrow_function(self.hover_style)
        self.geojson = self.retrieve_geojson(geojson_path, namespace, hideout, hover_style)

    def get_title(self, **kwargs):
        """ Return title from base title and elements """

        tstep = kwargs['day']
        self.base_title = WAS[self.was]['title']
        self.cdatetime = self.retrieve_cdatetime(tstep)
        title = super(ForecastWasFigureHandler, self).get_title()

        return title

    def get_figure_layers(self, day=0):
        """ Get figure layers (GeoJSON trace, legend and information box with title)

        Parameters
        ----------
        day : int, optional
            Day, by default 0

        Returns
        -------
        list
            Figure layers
        """

        logging.debug('Updating layout ...')
        
        if os.path.exists(self.filepath):
            # Get trace
            self.generate_var_tstep_trace(day)

            # Get figure title
            self.fig_title = html.P(html.B(
                [
                    item
                    for sublist in self.get_title(day=day).split('<br>')
                    for item in [sublist, html.Br()]
                ][:-1]
            ))
        else:
            self.geojson = None
            self.legend = None

            # Get figure title
            self.fig_title = html.P(html.B("DATA NOT AVAILABLE"))

        # Get title information element
        self.info = self.retrieve_info(self.name)

        return [self.geojson, self.legend, self.info]


class EvaluationGroundFigureHandler(PointsFigureHandler):
    """ Class which handles AERONET observations data """

    def __init__(self, sdate=None, edate=None, obs=None, var=None):
        """_summary_

        Parameters
        ----------
        sdate : str, optional
            Start date, by default None
        edate : str, optional
            End date, by default None
        obs : str, optional
            Observations name, by default None
        var : str, optional
            Variable name, by default None
        """

        self.name = obs
        super(EvaluationGroundFigureHandler, self).__init__()
        self.circle_options = OBS[obs]['circle_options']

        fday = sdate[:-2] + '01'
        lday = edate[:-2] + str(calendar.monthrange(int(edate[:4]), int(edate[4:6]))[1])
        date_range = pd.date_range(fday, lday, freq='M')
        months = [d.strftime("%Y%m") for d in date_range.to_pydatetime()]
        filepaths = ["{}.nc".format(os.path.join(OBS[obs]['path'], 'netcdf',
            OBS[obs]['template'].format(OBS[obs]['obs_var'], month))) for month
            in months]
        self.input_files = [nc_file(filepath) for filepath in filepaths if os.path.exists(filepath)]
        
        if self.input_files:

            if 'longitude' in self.input_files[0].variables:
                lon_var = 'longitude'
                lat_var = 'latitude'
            else:
                lon_var = 'lon'
                lat_var = 'lat'

            lon = self.input_files[0].variables[lon_var][:]
            lat = self.input_files[0].variables[lat_var][:]
            self.var = [var for var in self.input_files[0].variables if var == OBS[obs]['obs_var']][0]
            logging.debug('VARNAME %s', self.var)

            sites_path = os.path.join(DIR_PATH, 'conf/', OBS[obs]['sites'])
            if os.path.exists(sites_path):
                sites = pd.read_csv(sites_path)
                idxs, self.station_names = np.array([[idx, st_name[~st_name.mask].tobytes().decode('utf-8')]
                                    for idx, st_name in
                                    enumerate(self.input_files[0].variables['station_name'][:])
                                    if st_name[~st_name.mask].tobytes().decode('utf-8').upper() in map(str.upper, sites['SITE'])]
                                    ).T

                logging.debug('IDXS %s', idxs)
                logging.debug('ST_NAMES %s', self.station_names)

            self.clon = lon[idxs.astype(int)]
            self.clat = lat[idxs.astype(int)]

            self.values = {
                self.var: np.concatenate([input_file.variables[self.var][:, idxs.astype(int)] 
                                         for input_file in self.input_files])
            }

        return None

    def generate_var_tstep_trace(self):
        """ Generate GeoJSON trace to be added """

        val = np.ma.masked_where(self.values[self.var]<0., self.values[self.var])
        notnan = (np.array([i for i in range(val.shape[1]) if not val[:, i].mask.all()]),)
        clon = self.clon[notnan]
        clat = self.clat[notnan]
        cstations = self.station_names[notnan]

        # Create dataframe
        self.df = pd.DataFrame({
            'lon': clon.round(2),
            'lat': clat.round(2),
            'stations': cstations
            })  # .T, columns=['lon', 'lat', 'station'])
        dicts = self.df.to_dict('records')

        # Create geojson
        # Note: It must be JavaScript as it is executed in clientside
        geojson_data = dlx.dicts_to_geojson(dicts, lon="lon")
        namespace = Namespace("evaluationTab", "evaluationMaps")
        self.geojson = self.retrieve_geojson(geojson_data, namespace)

        return None

    def get_figure_layers(self):
        """ Get dataframe and figure layers (GeoJSON trace)

        Returns
        -------
        pandas.core.frame.DataFrame
            Dataframe with stations information
        list
            Figure layers
        """

        logging.debug('Updating layout ...')

        if self.input_files:
            # Get trace
            self.generate_var_tstep_trace()

        else:
            self.geojson = None
            self.df = pd.DataFrame([])

        return self.df, [self.geojson]


class EvaluationSatelliteFigureHandler(ContourFigureHandler):
    """ Class which handles MODIS sensor data """

    def __init__(self, var, obs, tstep, selected_date):
        """ Initialize EvaluationSatelliteFigureHandler class

        Parameters
        ----------
        var : str
            Variable name
        obs : str
            Observations name
        tstep : int
            Timestep
        selected_date : str
            Selected date
        """

        self.obs = obs
        self.name = self.obs
        super(EvaluationSatelliteFigureHandler, self).__init__()
        
        self.var = var
        self.selected_date = selected_date

        self.bounds = np.array(VARS[self.var.upper()]['bounds']).astype('float32')
        self.style = dict(weight=0, opacity=0, color='white', dashArray='', fillOpacity=OPACITY)
        self.colorscale = COLORS

        if self.selected_date:
            # Get NetCDF file path
            filedir = OBS[self.obs]['path']
            filetpl = OBS[self.obs]['template'].format(OBS[self.obs]['obs_var'], 
                                                       self.selected_date) + '.nc'
            self.filepath = os.path.join(filedir, 'netcdf', filetpl)

            if os.path.exists(self.filepath):
                # Read NetCDF file and retrieve timesteps, what (hours, months, etc.) and rdatetime
                self.timesteps, self.what, self.rdatetime = self.get_time_details()

        return None

    def generate_var_tstep_trace(self, tstep):
        """ Generate GeoJSON trace to be added per timestep

        Parameters
        ----------
        tstep : int
            Timestep
        """

        namespace = Namespace("forecastTab", "forecastMaps")
        data_path = os.path.basename(OBS[self.obs]['path'][:-1])
        geojson_path = os.path.join('geojsons', GEOJSON_TEMPLATE.format(data_path, 
            self.selected_date, tstep, self.selected_date, OBS[self.obs]['obs_var']))
        self.geojson = self.retrieve_geojson(geojson_path, namespace)

        return None

    def generate_colorbar(self):
        """ Generate colorbar """

        ctg = ["{:d}".format(int(cls)) if cls.as_integer_ratio()[1] == 1 else "{:.1f}".format(cls) 
               for i, cls in enumerate(self.bounds[1:-1])]
        indices = list(range(len(ctg) + 2))
        self.colorbar_info.update({'min': 0,
                                   'max': len(ctg)+1, 
                                   'classes': indices,
                                   'colorscale': COLORS,
                                   'tickValues': indices[1:-1],
                                   'tickText': ctg})
        self.colorbar = self.retrieve_colorbar()

        logging.debug("BOUNDS %s", self.bounds)
        logging.debug("CTG %s", ctg)

        return None

    def get_title(self, **kwargs):
        """ Return title from base title and elements """

        varname = kwargs['varname']
        tstep = kwargs['tstep']

        self.base_title = " ".join([OBS[self.obs]['name'], OBS[self.obs]['title']])
        self.cdatetime = self.retrieve_cdatetime(tstep)
        self.step = "{:02d}".format(tstep*FREQ)
        title = super(EvaluationSatelliteFigureHandler, self).get_title()
        
        return title

    def get_figure_layers(self, tstep=0):
        """ Get figure layers (GeoJSON trace, colorbar and information box with title)

        Parameters
        ----------
        tstep : int, optional
            Timestep, by default 0

        Returns
        -------
        list
            Figure layers
        """

        logging.debug('Updating layout ...')

        if self.var and os.path.exists(self.filepath):
            # Get trace and colorbar
            self.generate_colorbar()
            self.generate_var_tstep_trace(tstep)

            # Get figure title
            self.fig_title = html.P(html.B(
                [
                    item for sublist in self.get_title(varname=self.var, tstep=tstep).split('<br>') 
                    for item in [sublist, html.Br()]
                ][:-1]
            ))
        else:
            self.geojson = None
            self.colorbar = None

            # Get figure title
            if self.obs in OBS[self.obs]:
                title = "{} - DATA NOT AVAILABLE".format(OBS[self.obs]['name'])
            else:
                title = "DATA NOT AVAILABLE"
            self.fig_title = html.P(html.B(title))

        # Get title information element
        self.info = self.retrieve_info(self.name)
        
        return [self.geojson, self.colorbar, self.info]


class EvaluationStatisticsFigureHandler(PointsFigureHandler):
    """ Class which handles evaluation statistics data """

    def __init__(self, network=None, statistic=None, model=None, selection=None):
        """ Initialize EvaluationStatisticsFigureHandler class

        Parameters
        ----------
        network : str, optional
            Network name, by default None
        statistic : str, optional
            Statistic name, by default None
        model : str, optional
            Model name, by default None
        selection : str, optional
            Period selection, by default None
        """

        self.name = 'scores'
        super(EvaluationStatisticsFigureHandler, self).__init__()

        # self.extent = SCORES['extent']
        self.model = model
        self.network = network
        self.statistic = statistic
        self.selection = selection

        if self.network and self.model and self.statistic and self.selection:

            # Define markers style
            self.bounds = np.array(STATS_CONF[self.statistic]['bounds'])
            self.cmap = cm.get_cmap(STATS_CONF[self.statistic]['cmap'], len(self.bounds))
            self.colormap = {}
            for i in range(self.cmap.N):
                self.colormap[i] = matplotlib.colors.rgb2hex(self.cmap(i))
            self.labels = list(self.colormap.keys())
            self.colors = list(self.colormap.values())
            self.circle_options = SCORES['circle_options']

            # Get stations data
            if network == 'aeronet':
                self.sites = pd.read_csv(os.path.join(DIR_PATH, 'conf/', OBS[network]['sites']))
                self.circle_options.update({'radius': 6})
            else:
                self.sites = None
                self.circle_options.update({'radius': 2})
            
            # Get selected month / year
            # Set day to first date of the month to transform into datetime and avoid errors creating title
            year = int(self.selection[0:4])
            month = int(self.selection[4:6])
            self.rdatetime = dt(year, month, 1)

            # Read data
            self.read_data()

        return None

    def read_data(self):
        """ Read data """

        # Get HDF5 file
        filedir = OBS[self.network]['path']
        filename = "{}_{}.h5".format(self.selection, self.statistic)
        tab_name = "{}_{}".format(self.statistic, self.selection)
        self.filepath = os.path.join(filedir, "h5", filename)

        # Read data
        if os.path.exists(self.filepath):
            self.data = pd.read_hdf(self.filepath, tab_name)
            logging.debug('SCORES FILEPATH %s SELECTION %s TAB %s', self.filepath,  self.selection, tab_name)

        return None

    def select_data(self):
        """ Select data

        Returns
        -------
        numpy.ndarray
            Longitudes
        numpy.ndarray
            Latitudes
        numpy.ndarray
            Stations
        numpy.ndarray 
            Scores
        numpy.ndarray
            Correspondence on legend scale
        """

        # Get coordinates for each site
        if self.sites is not None:
            for station in self.sites['SITE']:
                self.data.loc[self.data['station'] == station, 'lon'] = \
                    self.sites.loc[self.sites['SITE'] == station, 'LONGITUDE'].values[0]
                self.data.loc[self.data['station'] == station, 'lat'] = \
                    self.sites.loc[self.sites['SITE'] == station, 'LATITUDE'].values[0]
    
        # Transform empty values into np.nan
        data = self.data.replace('-', np.nan)

        # Remove continents and stations without coordinates / model score
        data = data.dropna(subset=['lat', 'lon', self.model])

        # Transform into numeric (if needed)
        for column in ['lat', 'lon', self.model]:
            data[column] = pd.to_numeric(data[column])

        # Get coordinates / scores / stations for selected model
        lon = np.array(data['lon'])
        lat = np.array(data['lat'])
        scores = np.array(data[self.model])
        if self.sites is not None:
            stations = np.array(data['station'])
        else:
            stations = None
            
        # Assign color index in colormap to scores
        res = np.zeros((len(lon)))
        for point_n, score in enumerate(scores):
            for label_n, label in enumerate(self.labels):
                # Skip last position
                if label_n < self.labels[-1]:
                    # Below minimum limit, get first color in cmap
                    if float(score) < self.bounds[0]:
                        res[point_n] = self.labels[0]
                        break
                    # Above maximum limit, get last color in cmap
                    elif float(score) >= self.bounds[-1]:
                        res[point_n] = self.labels[-1]
                        break
                    # In between, find cmap color in range
                    elif (float(score) >= self.bounds[label_n]) and (float(score) < self.bounds[label_n+1]):
                        res[point_n] = self.labels[label_n]
                        break

        return lon, lat, stations, scores, res

    def generate_var_tstep_trace(self, lon, lat, stations, scores, res):
        """ Generate GeoJSON trace to be added

        Parameters
        ----------
        lon : numpy.ndarray
            Longitudes
        lat : numpy.ndarray
            Latitudes
        stations : numpy.ndarray
            Stations
        scores : numpy.ndarray
            Scores
        res : numpy.ndarray
            Correspondence on legend scale
        """

        # Create dataframe
        df = pd.DataFrame({
            'lon': np.array(lon).round(2),
            'lat': np.array(lat).round(2),
            'score': scores.round(2),
            'value': res
            })
        if stations is not None:
            df['station'] = stations
            var_list=['station', 'lon', 'lat', 'score']
        else:
            var_list=['lon', 'lat', 'score']
        
        # Add tooltips (hover information) to map
        data_dict = self.get_tooltip(df, var_list=var_list)

        # Create geojson
        geojson_data = dlx.dicts_to_geojson(data_dict, lon="lon")
        namespace = Namespace("evaluationTab", "evaluationMaps")
        self.geojson = self.retrieve_geojson(geojson_data, namespace)

    def generate_colorbar(self):
        """ Generate colorbar """

        bounds = STATS_CONF[self.statistic]['bounds']
        ctg = ["{}".format(cls) if '.' in str(cls) else "{:d}".format(cls)
                for i, cls in enumerate(bounds)]
        indices = list(range(len(bounds)))
        self.colorbar_info.update({'min': bounds[0],
                                   'max': bounds[-1],
                                   'classes': len(bounds)-1,
                                   'colorscale': self.colors,
                                   'tickValues': self.bounds,
                                   'tickText': ctg})
        self.colorbar = self.retrieve_colorbar()

        return None

    def get_title(self, **kwargs):
        """ Return title from base title and elements """

        self.model_name = MODELS[self.model]['name']
        self.network_name = OBS[self.network]['name']
        self.statistic_name = STATS_CONF[self.statistic]['name']
        self.base_title = SCORES['title']
        title = super(EvaluationStatisticsFigureHandler, self).get_title()
        
        return title

    def get_figure_layers(self):
        """ Get figure layers (GeoJSON trace, colorbar and information box with title)

        Returns
        -------
        list
            Figure layers
        """

        logging.debug('Updating layout ...')

        if (os.path.exists(self.filepath)) and (self.model in self.data.columns):
            # Get data
            lon, lat, stations, scores, res = self.select_data()

            # Get trace and colorbar
            self.generate_colorbar()
            self.generate_var_tstep_trace(lon, lat, stations, scores, res)

            # Get figure title
            self.fig_title = html.P(html.B(
                [
                    item for sublist in self.get_title().split('<br>') 
                    for item in [sublist, html.Br()]
                ][:-1]
            ))
        else:
            self.geojson = None
            self.colorbar = None
            self.fig_title = html.P(html.B("DATA NOT AVAILABLE"))

        # Get title information element
        self.info = self.retrieve_info(self.name)

        return [self.geojson, self.colorbar, self.info]


class VisFigureHandler(PointsFigureHandler):
    """ Class which handles visibility data """

    def __init__(self, selected_date=None):
        """ Initialize VisFigureHandler class

        Parameters
        ----------
        selected_date : str, optional
            Selected date, by default None
        """

        self.name = 'vis'
        super(VisFigureHandler, self).__init__()

        self.selected_date = selected_date

        self.path_tpl = VIS['path']
        self.xlon = np.array(VIS['xlon'])
        self.ylat = np.array(VIS['ylat'])
        self.ec = VIS['ec']
        self.size = VIS['size']
        self.freq = VIS['freq']
        self.colormap = VIS['colormap']
        self.colors = list(VIS['colormap'].values())
        self.labels = list(VIS['colormap'].keys())
        self.values = VIS['values']
        self.circle_options = VIS['circle_options']

        if self.selected_date:
            self.rdatetime = dt.strptime(self.selected_date, '%Y%m%d')

        return None

    def read_data(self, tstep):
        """ Read data

        Parameters
        ----------
        tstep : int
            Timestep
        """

        # Get CSV file
        tstep0 = tstep
        tstep1 = tstep + self.freq
        logging.info("%s %s %s", self.selected_date, tstep0, tstep1)
        year = dt.strptime(self.selected_date, '%Y%m%d').strftime('%Y')
        month = dt.strptime(self.selected_date, '%Y%m%d').strftime('%m')
        day = dt.strptime(self.selected_date, '%Y%m%d').strftime('%d')
        self.filepath = self.path_tpl.format(year=year, month=month, day=day, tstep0=tstep0, 
                                             tstep1=tstep1)
        
        # Read data
        if os.path.exists(self.filepath):
            self.data = pd.read_table(self.filepath, na_filter=False)
            logging.debug("VIS FILEPATH %s", self.filepath)

        return None

    def select_data(self, tstep=0):
        """ Select data

        Parameters
        ----------
        tstep : int, optional
            Timestep, by default 0

        Returns
        -------
        numpy.ndarray
            Longitudes
        numpy.ndarray
            Latitudes
        numpy.ndarray
            Stations
        numpy.ndarray
            Visibilities
        numpy.ndarray
            Relative humidities
        numpy.ndarray
            Correspondence on legend scale
        """

        # Uncertain
        cx = np.where((self.data['WW'].astype(str) == "HZ") | (self.data['WW'].astype(str) == "5")| (self.data['WW'].astype(str) == "05"))

        # Visibility <= 1km
        c0t = np.where((self.data['VV'] <= 1000))[0]
        c0x = np.where([c0t == i for i in cx[0]])[-1]
        c0 = (np.delete(c0t, c0x),)

        # Visibility 1km <= 2km
        c1t = np.where((self.data['VV'] > 1000) & (self.data['VV'] <= 2000))[0]
        c1x = np.where([c1t == i for i in cx[0]])[-1]
        c1 = (np.delete(c1t, c1x),)

        # Visibility 2km <= 5km
        c2t = np.where((self.data['VV'] > 2000) & (self.data['VV'] <= 5000))[0]
        c2x = np.where([c2t == i for i in cx[0]])[-1]
        c2 = (np.delete(c2t, c2x),)

        lon = self.data['LON'].values
        lat = self.data['LAT'].values
        stations = self.data['STATION'].values
        visibility = self.data['VV'] if 'VV' in self.data.columns else []
        humidity = self.data['HUMIDITY'] if 'HUMIDITY' in self.data.columns else []

        # Make sure that the dtype is not object, this appears in files with missing values
        humidity = pd.to_numeric(humidity, errors='coerce')
        
        # Replace np.nan by -999
        # In the case of humidity, we want to show the values as int and cannot do so with nan
        humidity = humidity.replace(float('nan'), -999)

        # Assign colors to values
        n_points = len(lon)
        res = np.zeros((n_points))
        values = (c0, c1, c2, cx)
        for i, (value, label) in enumerate(zip(values, self.labels)):
            res[value] = i

        return lon, lat, stations, np.array(visibility), np.array(humidity), res

    def generate_var_tstep_trace(self, lon, lat, stations, visibility, humidity, res, tstep=0):
        """ Generate GeoJSON trace to be added per timestep

        Parameters
        ----------
        lon : numpy.ndarray
            Longitudes
        lat : numpy.ndarray
            Latitudes
        stations : numpy.ndarray
            Stations
        visibility : numpy.ndarray
            Visibilities
        humidity : numpy.ndarray
            Humidities
        res : tuple
            Correspondence on legend scale
        tstep : int, optional
            Timestep, by default 0
        """

        if list(lon) and list(lat) and list(stations) and list(visibility) and list(humidity):

            # Create legend
            self.legend = self.create_legend(self.colormap)

            # Create dataframe
            df = pd.DataFrame({
                'station': stations,
                'lon': lon.round(2),
                'lat': lat.round(2),
                'visibility': (visibility/1e3).round(2),
                'humidity': humidity.astype(int),
                'value': res
                })
        
            # Replace -999 by np.nan to revert previous change
            df['humidity'] = df['humidity'].replace(-999, float('nan'))

            # Add tooltips (hover information) to map
            data_dict = self.get_tooltip(df, 
                var_list=['station', 'lon', 'lat', 'visibility', 'humidity'])

            # Create geojson
            geojson_data = dlx.dicts_to_geojson(data_dict, lon="lon")
            namespace = Namespace("observationsTab", "observationsMaps")
            self.geojson = self.retrieve_geojson(geojson_data, namespace)

        return None

    def get_title(self, **kwargs):
        """ Return title from base title and elements """

        tstep = None if 'tstep' not in kwargs else kwargs['tstep']
        if tstep is None:
            return "NO DATA AVAILABLE"
        else:
            self.base_title = VIS['title']
        
        self.tstep0 = "{:02d}".format(tstep)
        self.tstep1 = "{:02d}".format(tstep + self.freq)
        title = super(VisFigureHandler, self).get_title()

        return title

    def get_figure_layers(self, tstep=0):
        """ Get figure layers (GeoJSON trace, legend and information box with title)

        Parameters
        ----------
        tstep : int, optional
            Timestep, by default 0

        Returns
        -------
        list
            Figure layers
        """

        logging.debug('Updating layout ...')

        # Read data
        if self.selected_date:
            self.read_data(tstep)

        if os.path.exists(self.filepath):
            # Get trace
            tstep = int(tstep)
            xlon, ylat, stations, visibility, humidity, res = self.select_data(tstep)
            self.generate_var_tstep_trace(xlon, ylat, stations, visibility, humidity, res, tstep)
            
            # Get figure title
            self.fig_title = html.P(html.B(
                [
                    item for sublist in self.get_title(tstep=tstep).split('<br>') 
                    for item in [sublist, html.Br()]
                ][:-1]
            ))
        else:
            self.geojson = None
            self.legend = None

            # Get figure title
            self.fig_title = html.P(html.B("DATA NOT AVAILABLE"))

        # Get title information element
        self.info = self.retrieve_info(self.name)

        return [self.geojson, self.legend, self.info]
