from __future__ import annotations
from pathlib import Path

from pybi.core import DataSource
import pandas as pd

from typing import TYPE_CHECKING, Callable, Dict, List, Optional, Union, Any
import os

from pybi.core.components import (
    ContainerComponent,
    ColBoxComponent,
    ComponentTag,
    BoxComponent,
    FlowBoxComponent,
    TextComponent,
    UploadComponent,
    GridBoxComponent,
)
from pybi.core.components.reactiveComponent import EChart, Slicer, Table, TextValue
from pybi.core.dataSource import DataSourceField, DataSourceTable, DataView
from pybi.utils.dataSourceUtils import ds2sqlite_file_base64, ds2sqlite
from pybi.utils.data_gen import (
    json_dumps_fn,
    random_ds_name,
    random_dv_name,
    get_project_root,
)
from pybi.core.sql import Sql
import pybi.utils.sql as sqlUtils


if TYPE_CHECKING:
    from pybi.core.components import ReactiveComponent


class AppMeta:
    def __init__(self, app: App) -> None:
        self.__app = app

    def set_dbLocalStorage(self, on: bool):
        """
        是否开启数据库本地缓存
        """
        self.__app.dbLocalStorage = on
        return self


class App(ContainerComponent):
    def __init__(self) -> None:
        super().__init__(ComponentTag.App)
        self.dataSources: List[DataSource] = []
        self.dataViews: List[DataView] = []
        # self._data_cps: List[ReactiveComponent] = []
        self._with_temp_host_stack: List[ContainerComponent] = []
        self._clear_data = False
        self.dbLocalStorage = False
        self.__meta = AppMeta(self)

    @property
    def meta(self):
        return self.__meta

    def clear_all_data(self):
        self._clear_data = True

    def _get_temp_host(self):
        if self._with_temp_host_stack:
            return self._with_temp_host_stack[len(self._with_temp_host_stack) - 1]
        return None

    def set_source(self, data: pd.DataFrame, *, name: Optional[str] = None):
        name = name or random_ds_name()

        ds = DataSource(name, data)
        self.dataSources.append(ds)
        return DataSourceTable(ds.name, data.columns.tolist())

    def set_dataView(
        self,
        sql: str,
        exclude_source: Optional[List[DataSourceTable]] = None,
        *,
        name: Optional[str] = None,
    ):
        exclude_source = exclude_source or []
        name = name or random_dv_name()
        dv = DataView(name, Sql(sql))

        for es in exclude_source:
            dv.exclude_source(es.source_name)

        self.dataViews.append(dv)
        return DataSourceTable(dv.name, sqlUtils.extract_fields_head_select(sql))

    def add_upload(
        self,
        *,
        host: Optional[ContainerComponent] = None,
    ):

        cp = UploadComponent()

        host = host or self._get_temp_host() or self
        host._add_children(cp)

        return cp

    def add_text(
        self,
        text: str,
        *,
        host: Optional[ContainerComponent] = None,
    ):

        contexts = list(TextValue.extract_sql_from_text(text))

        cp = TextValue(contexts)

        host = host or self._get_temp_host() or self
        host._add_children(cp)

        return cp

    def add_slicer(
        self,
        field: Union[DataSourceField, DataSourceTable],
        *,
        host: Optional[ContainerComponent] = None,
    ):
        if isinstance(field, DataSourceTable):
            field = field[field.columns[0]]

        assert isinstance(field, DataSourceField)
        sql = f"select distinct {field.name} from {field.source_name}"
        cp = Slicer(Sql(sql))
        cp.title = field.name
        cp.add_updateInfo(field.source_name, field.name)

        host = host or self._get_temp_host() or self
        host._add_children(cp)

        return cp

    def add_table(
        self,
        dataSourceTable: DataSourceTable,
        *,
        host: Optional[ContainerComponent] = None,
    ):

        sql = f"select {','.join(dataSourceTable.columns)} from {dataSourceTable.source_name}"
        cp = Table(Sql(sql))

        host = host or self._get_temp_host() or self
        host._add_children(cp)

        return cp

    def add_echart(
        self,
        options: Dict,
        *,
        host: Optional[ContainerComponent] = None,
    ):

        cp = EChart(options)

        for info in EChart.extract_infos_from_option(options):
            cp.add_sql_path(info.path, info._sql.sql)

        host = host or self._get_temp_host() or self
        host._add_children(cp)

        return cp

    def flowBox(
        self,
        *,
        host: Optional[ContainerComponent] = None,
    ):
        cp = FlowBoxComponent(self)

        host = host or self._get_temp_host() or self
        host._add_children(cp)

        return cp

    def gridBox(
        self,
        areas: Union[List[List[str]], str],
        *,
        host: Optional[ContainerComponent] = None,
    ):
        if isinstance(areas, str):
            areas = GridBoxComponent.areas_str2array(areas)

        cp = GridBoxComponent(areas, self)

        host = host or self._get_temp_host() or self
        host._add_children(cp)

        return cp

    def colBox(
        self,
        spec: List[int] | None = None,
        *,
        host: Optional[ContainerComponent] = None,
    ):
        cp = ColBoxComponent(spec, self)

        host = host or self._get_temp_host() or self
        host._add_children(cp)

        return cp

    def box(
        self,
        *,
        host: Optional[ContainerComponent] = None,
    ):
        cp = BoxComponent(self)
        host = host or self._get_temp_host() or self
        host._add_children(cp)
        return cp

    def save_zip_db(self, path: str):
        with open(path, mode="w", encoding="utf8") as f:
            f.write(ds2sqlite_file_base64(self.dataSources))

    def save_db(self, path: str):
        if Path(path).exists():
            os.remove(path)
        ds2sqlite(path, self.dataSources)

    def _to_json_dict(self):
        data = super()._to_json_dict()

        data["dbFile"] = ds2sqlite_file_base64(
            self.dataSources, clear_data=self._clear_data
        )
        return data

    def to_json(self):
        return json_dumps_fn(self, indent=2, ensure_ascii=False)

    def to_raw_html(self):
        symbol = '"__{{__config_data__}}___"'
        # scripts_tag = "<!-- [__other_scripts__] -->"

        # def script2module(name: str):
        #     file = get_project_root() / "template" / __scripts_mapping__[name]
        #     return f"<script>{file.read_text()}</script>"

        # script_codes = " ".join(script2module(s) for s in self._scripts)

        # config = self.create_config()
        # self.__reset_data()
        config = json_dumps_fn(self, ensure_ascii=False)

        with open(
            get_project_root() / "template/index.html", mode="r", encoding="utf8"
        ) as html:
            res = (
                html.read().replace(symbol, config)
                # .replace('<div id="app"></div>', f'<div id="app"></div> {script_codes}')
            )
            return res

    def to_html(self, file):
        file = Path(file)
        raw = self.to_raw_html()
        Path(file).write_text(raw, "utf8")

        print(f"to html:{file.absolute()}")
