# Copyright IBM All Rights Reserved.
# SPDX-License-Identifier: Apache-2.0

from abc import ABC
from typing import Dict, Optional, List

import dash
from dash import dcc, html, Output, Input, State
import dash_bootstrap_components as dbc
from dash_bootstrap_templates import load_figure_template
import os
from flask_caching import Cache
import enum
import diskcache
from dash.long_callback import DiskcacheLongCallbackManager

from dse_do_dashboard.utils.dash_common_utils import ScenarioTableSchema


class HostEnvironment(enum.Enum):
    Local = 1  # Regular Dash
    CPD402 = 2  # Special handling of port


class DashApp(ABC):
    def __init__(self, logo_file_name: str = 'IBM.png',
                 navbar_brand_name: Optional[str] = 'Dashboard',
                 cache_config: Dict = {},
                 port: int = 8050,
                 dash_debug: bool = False,
                 host_env: Optional[HostEnvironment] = None,
                 bootstrap_theme = dbc.themes.BOOTSTRAP,
                 bootstrap_figure_template: str = "bootstrap",
                 enable_long_running_callbacks: bool = False,
                 ):
        self.port = port
        self.host_env = host_env
        self.dash_debug = dash_debug
        self.bootstrap_theme = bootstrap_theme
        self.set_bootstrap_figure_template(bootstrap_figure_template)

        # Long running callbacks:
        self.enable_long_running_callbacks = enable_long_running_callbacks
        if self.enable_long_running_callbacks:
            cache = diskcache.Cache("./cache")
            self.long_callback_manager = DiskcacheLongCallbackManager(cache)
        else:
            self.long_callback_manager = None

        self.app = self.create_dash_app()

        # Margins to layout the header, sidebar and content area:
        self.header_height = "3rem" # "6rem"
        self.footer_height = "10rem"
        self.sidebar_width = "16rem"
        self.adbar_width = "4rem"

        self.logo_file_name = logo_file_name  # set before call to get_app_layout()
        self.navbar_brand_name = navbar_brand_name
        self.cache_config = cache_config

        self.config_cache()
        self.app.layout = self.get_app_layout()

        self.set_cache_callbacks()
        self.set_dash_callbacks()

        # To run locally on Mac, you need to specifically set host='localhost'
        self.run_server_kwargs: Dict = {}  # {"host": "localhost"}

    def set_run_server_kwargs(self, **run_server_kwargs):
        """Use to set input arguments for Dash.run_server(), in addition to `debug` and `port`.
        For instance, running locally on a Mac requires host='localhost'. """
        if run_server_kwargs is not None:
            self.run_server_kwargs = run_server_kwargs

    def set_bootstrap_figure_template(self, bootstrap_figure_template: str):
        """See https://hellodash.pythonanywhere.com/theme_explorer"""
        load_figure_template(bootstrap_figure_template)
        self.bootstrap_figure_template = bootstrap_figure_template

    def create_dash_app(self):
        """Creates the Dash app. Called from the DashApp constructor.
        Implements special process around running in CPDv4.0.2 (i.e. `ws_applications.make_link()`)
        Can be overridden if necessary.

        Note:
            Changing Bootstrap stylesheets requires a Reload on the browser!
        """
        # print(os.getcwd())
        assets_path = os.getcwd() +'/assets'  # Not sure why it is not picking it up automatically, but this works
        if self.host_env == HostEnvironment.CPD402:
            print(f"Host is CPD, so run with make_link({self.port})")
            from ws_applications import make_link
            requests_prefix = make_link(self.port)
            app = dash.Dash(__name__,
                            serve_locally=False,
                            requests_pathname_prefix=requests_prefix,
                            # suppress_callback_exceptions = True,
                            assets_folder=assets_path,
                            long_callback_manager=self.long_callback_manager,
                            )
        else:
            app = dash.Dash(__name__,
                            # suppress_callback_exceptions = True,
                            assets_folder=assets_path,
                            long_callback_manager=self.long_callback_manager,
                            )
        dbc_css = "https://cdn.jsdelivr.net/gh/AnnMarieW/dash-bootstrap-templates/dbc.css"
        app.config.external_stylesheets = [self.bootstrap_theme, dbc_css]
        # app.config.external_stylesheets = [dbc.themes.SOLAR]
        app.config.suppress_callback_exceptions = True
        return app

    def run_server(self):
        """Runs the Dash server.
        To be called from index.py::

            if __name__ == '__main__':
                DA.run_server()

        """
        self.app.run_server(debug=self.dash_debug, port=self.port, **self.run_server_kwargs)

    def config_cache(self):
        self.cache = Cache()
        self.cache.init_app(self.app.server, config=self.cache_config)

    def get_app_layout(self):
        """Layout of whole app"""
        layout = html.Div([
            dcc.Location(id='url'),
            self.get_app_stores_div(),
            self.get_navbar(),
            self.get_sidebar(),
            self.get_content_template()],
            className="dbc"  # Bootstrap CSS, see https://hellodash.pythonanywhere.com/about_dbc_css
        )
        return layout

    def display_content_callback(self, pathname):
        """Callback for main content area. Will update the content area.
        Will need to be called through callback in index.py, where `DA` is the instance of the DashApp.

        Usage::

            @app.callback(Output('content', 'children'), [Input('url', 'pathname')])
            def display_content(pathname):
                return DA.display_content_callback()

        """
        layout = html.Div([
            html.H1(f"Error 404 - Page '{pathname}' not found"),
            dcc.Textarea(
                id='not_found',
                value='Page not found',
                style={'width': '100%', 'height': 300},
            ),
        ])
        return layout

    def get_content_template(self):
        """The template structure for the main content page.
        The callback `display_content` will update the actual contents
        """
        content = html.Div(id = 'content',
                           style = self.get_content_style()
                           )
        return content

    def get_content_style(self):
        """Style for the main contents section.
        Very important is the `margin-left`, otherwise it will overlap with the left-side navbar.
        Except for the margins, the other properties don't seem to differ from the default.

        :return:
        """
        # header_height, footer_height = "6rem", "10rem"
        # sidebar_width, adbar_width = "16rem", "4rem"
        content_style = {
            "margin-top": "1rem" , #self.header_height,
            "margin-left": self.sidebar_width,
            "margin-right": "1rem" , #self.adbar_width,
            "margin-bottom": self.footer_height,
            # "padding": "2rem 1rem",
            # "card_border": {
            #     "width": "1px",
            #     "style": "solid",
            #     "color": "#8EA9C1",
            #     "radius": "0px"
            # },
            # "card_background_color": "#edf3f4",
            # "card_box_shadow": "0px 0px 0px rgba(0,0,0,0)",
            # "card_margin": "15px",
            # "card_padding": "5px",
            # "card_outline": {
            #     "width": "0px",
            #     "style": "solid",
            #     "color": "#8EA9C1"
            # },
            # "card_header_border": {
            #     "width": "0px 0px 2px 0px",
            #     "style": "dashed",
            #     "color": "#8EA9C1",
            #     "radius": "0px"
            # },
            # "card_header_background_color": "#edf3f4",
            # "card_header_box_shadow": "0px 0px 0px rgba(0,0,0,0)",
            # "card_header_margin": "0px",
            # "card_header_padding": "10px",
            # "colorway": [
            #     "#004172",
            #     "#08539d",
            #     "#2e64c7",
            #     "#be35a0",
            #     "#e32433",
            #     "#eb6007",
            #     "#fb8b00",
            #     "#c19f00",
            #     "#5c9c00",
            #     "#897500",
            #     "#cb0049",
            #     "#7746ba",
            #     "#0080d1",
            #     "#3192d2",
            #     "#ac6ac0",
            #     "#e34862",
            #     "#c57e00",
            #     "#71a500",
            #     "#ad6e00",
            #     "#b82e2e",
            # ],
            # "colorscale": [
            #     "#00224e",
            #     "#123570",
            #     "#3b496c",
            #     "#575d6d",
            #     "#707173",
            #     "#8a8678",
            #     "#a59c74",
            #     "#c3b369",
            #     "#e1cc55",
            #     "#fee838"
            # ],
            # "font_family": "Quicksand",
            # "font_size": "15px",
            # "font_size_smaller_screen": "13px",
            # "font_family_header": "PT Sans",
            # "font_size_header": "24px",
            # "font_family_headings": "PT Sans",
            # "font_headings_size": None,
            # "header_border": {
            #     "width": "0px",
            #     "style": "solid",
            #     "color": "#8EA9C1",
            #     "radius": "0px"
            # },
        }
        return content_style

    def get_logo_src(self):
        src = self.app.get_asset_url(self.logo_file_name)
        return src

    def get_navbar_brand_children(self) -> str:
        """The children of the dbc.NavbarBrand component, typically a plain string.
        To be overridden."""
        brand = self.navbar_brand_name  #'Dashboard'
        return brand

    def get_navbar(self):
        # The scenario-bar is the section on the right with the scenario-dropdown and the refresh button
        scenario_bar = dbc.Row(
            [
                dcc.Store(id='reference_scenario_name_store'),
                dcc.Store(id='multi_scenario_names_store'),
                dbc.Col(
                    dcc.Dropdown(
                        id='top_menu_scenarios_drpdwn',
                        style={'width': '30vw'},
                    ),
                ),
                dbc.Col(
                    dbc.Button(
                        "Refresh", id='scenario_refresh_button', color="primary", className="ms-2", n_clicks=0
                    ),
                    width="auto",
                ),
            ],
            className="g-0 ms-auto flex-nowrap mt-3 mt-md-0",
            align="center",
        )

        navbar = dbc.Navbar(
            dbc.Container(
                [
                    html.A(
                        # Use row and col to control vertical alignment of logo / brand
                        dbc.Row(
                            [
                                dbc.Col(html.Img(src=self.get_logo_src(), height="50vh")),
                                # dbc.Col(dbc.NavbarBrand("Dashboard", className="ms-2")),
                                dbc.Col(dbc.NavbarBrand(self.get_navbar_brand_children(), className="ms-2")),
                            ],
                            align="center",
                            className="g-0",
                        ),
                        # href="/",
                        style={"textDecoration": "none"},
                    ),
                    # scenario_bar
                    dbc.NavbarToggler(id="navbar-toggler", n_clicks=0),  # The NavbarToggler is a hamburger menu that will only appear of the screen is very small
                    dbc.Collapse(
                        scenario_bar,
                        id="navbar-collapse",
                        is_open=False,
                        navbar=True,
                    ),
                ]
            ),
            color="dark",
            dark=True,
        )
        return navbar
    # def get_navbar(self):
    #     navbar = dbc.Navbar(
    #         dbc.Container(
    #             [
    #                 html.A(
    #                     # Use row and col to control vertical alignment of logo / brand
    #                     dbc.Row(
    #                         [
    #                             dbc.Col(html.Img(src=self.get_logo_src(), height="50vh")),
    #                             dbc.Col(dbc.NavbarBrand("Dashboard", className="ms-2")),
    #                         ],
    #                         # align="left",
    #                         className="g-0",
    #                     ),
    #                     style={"textDecoration": "none"},
    #                 ),
    #                 dcc.Dropdown(
    #                     id='top_menu_scenarios_drpdwn',
    #                     style={'width': '20vw'},
    #
    #                 ),
    #
    #                 html.Button('Refresh', id='scenario_refresh_button', n_clicks=0,
    #                             style={'width': '300px'}
    #                             ),
    #             ]
    #         ),
    #         color="dark",
    #         dark=True,
    #         fixed='top'
    #     )
    #     return navbar

    def get_sidebar(self):
        sidebar_style = {
            "position": "fixed",
            "top": self.header_height,
            "left": 0,
            "bottom": self.footer_height,
            "width": self.sidebar_width,
            "padding": "1rem 1rem",
        }
        sidebar = html.Div([
            html.Hr(),
            dbc.Nav([

                dbc.NavLink(
                    href=self.app.get_relative_path('/'),
                    children='Home'
                ),
                # dbc.NavLink(
                #     href=self.app.get_relative_path('/prepare-data'),
                #     children='Prepare Data'
                # ),
                # dbc.NavLink(
                #     href=self.app.get_relative_path('/run-model'),
                #     children='Run Model'
                # ),
                # dbc.NavLink(
                #     href=self.app.get_relative_path('/explore-solution'),
                #     children='Explore Solution'
                # ),
                # dbc.NavLink(
                #     href=app.get_relative_path('/visualization'),
                #     children='Visualization'
                # ),

                # dbc.Button(
                #     "Visualization",
                #     outline=True,
                #     id = 'collapse-button',
                #     className='mb-3',
                #     n_clicks=0,
                #     href=self.app.get_relative_path('/visualization'),
                #     # active=True,
                #     color='primary',
                #
                # ),

                # dbc.Collapse(
                #     children = visualization_menu_children,
                #     id = "collapse",
                #     is_open=True,
                #     navbar=True,
                # ),

            ], vertical=True, pills=True,)], style = sidebar_style
        )
        return sidebar

    def get_app_stores_div(self) -> html.Div:
        """A Div container with all globally (and thus permanently available) dcc.Stores in the app."""
        stores = html.Div(
            id = 'global_stores',  # Not necessary
            children = self.get_app_stores()
        )
        return stores

    def get_app_stores(self) -> List[dcc.Store]:
        """To be overridden"""
        stores = []
        return stores

    def set_cache_callbacks(self):
        pass

    def set_dash_callbacks(self):
        app = self.app

        @app.callback(
            Output('content', 'children'),
            [Input('url', 'pathname'), Input('top_menu_scenarios_drpdwn', 'value')],
            [State('reference_scenario_name_store', 'data'),
             State('multi_scenario_names_store', 'data')
             ]
        )
        def display_content(pathname, scenario_name, reference_scenario_name, multi_scenario_names):
            """High level call back to update the content section of the app.
            Triggers when either the URL or the scenario-dropdown changes."""
            # reference_scenario_name = None,  #TODO
            # multi_scenario_names = None  #TODO

            # print(f"Reference scenario = {reference_scenario_name}")
            # print(f"Multi scenario names = {multi_scenario_names}")
            print(f"Showing URL = {pathname}")
            return self.display_content_callback(pathname, scenario_name, reference_scenario_name, multi_scenario_names)

        @app.callback(
            [Output('top_menu_scenarios_drpdwn', 'options'), Output('top_menu_scenarios_drpdwn', 'value')],
            [Input('scenario_refresh_button', 'n_clicks')],
            [State('top_menu_scenarios_drpdwn', 'value')],
            # Read the state of scenarios dropdown. Do NOT define this as an Input, otherwise the refresh will be triggered when you change the scenario.
            # prevent_initial_call = True  # If True, would prevent clearing the cache. However, it will also prevent the initialization of the scenario drpdwn menu!
        )
        def refresh_scenarios(n_clicks, current_scenario_name:str):
            """Reads the currently selected scenario-name. Reloads the scenario names from DB.
            If the currently selected scenario is valid, this will be maintained as the selected value.
            Main callback from scenario dropdown (?)
            """
            options, initial_scenario_name = self.refresh_scenarios_dash_callback(current_scenario_name)
            return [options, initial_scenario_name]

        @app.callback(
            Output("navbar-collapse", "is_open"),
            [Input("navbar-toggler", "n_clicks")],
            [State("navbar-collapse", "is_open")],
        )
        def toggle_navbar_collapse(n, is_open):
            """Callback for NavbarToggler in top-menubar.
            The NavbarToggler is a hamburger menu that will only appear of the screen is very small.
            So in normal desktop use, this toggler and callback are irrelevant."""
            if n:
                return not is_open
            return is_open

    def shutdown(self):
        """Shuts-down the Flash web-server, releasing the port.
        Relevant in CPD, so the port gets released immediately.
        """
        from flask import request
        func = request.environ.get('werkzeug.server.shutdown')
        if func is None:
            raise RuntimeError('Not running with the Werkzeug Server')
        func()

    def get_table_schema(self, table_name) -> Optional[ScenarioTableSchema]:
        pass
