import datetime
import os
from .project import *
from .database import ChangeSet, write
from .sections import *
from .s0_base import get_region_type
from .s1_title import inp_in_title
from .s2_junctions import inp_in_junction
from .s3_reservoirs import inp_in_reservoir
from .s4_tanks import inp_in_tank
from .s5_pipes import inp_in_pipe
from .s6_pumps import inp_in_pump
from .s7_valves import inp_in_valve
from .s8_tags import inp_in_tag
from .s9_demands import inp_in_demand
from .s10_status import inp_in_status
from .s11_patterns import pattern_v3_types, inp_in_pattern
from .s12_curves import curve_types, inp_in_curve
from .s13_controls import inp_in_control
from .s14_rules import inp_in_rule
from .s15_energy import inp_in_energy
from .s16_emitters import inp_in_emitter
from .s17_quality import inp_in_quality
from .s18_sources import inp_in_source
from .s19_reactions import inp_in_reaction
from .s20_mixing import inp_in_mixing
from .s21_times import inp_in_time
from .s22_report import inp_in_report
from .s23_options import inp_in_option
from .s23_options_v3 import inp_in_option_v3
from .s24_coordinates import inp_in_coord
from .s25_vertices import inp_in_vertex
from .s26_labels import inp_in_label
from .s27_backdrop import inp_in_backdrop
from .s32_region import inp_in_region, inp_in_bound, inp_in_regionnodes
from .s32_region_util import from_postgis_polygon, to_postgis_polygon

# DingZQ, 2024-12-28, export inp
from .inp_out import export_inp

_S = "S"
_L = "L"


def _inp_in_option(section: list[str], version: str = "3") -> str:
    return inp_in_option_v3(section) if version == "3" else inp_in_option(section)


_handler = {
    TITLE: (_S, inp_in_title),
    JUNCTIONS: (_L, inp_in_junction),  # line, demand_outside
    RESERVOIRS: (_L, inp_in_reservoir),
    TANKS: (_L, inp_in_tank),
    PIPES: (_L, inp_in_pipe),
    PUMPS: (_L, inp_in_pump),
    VALVES: (_L, inp_in_valve),
    TAGS: (_L, inp_in_tag),
    DEMANDS: (_L, inp_in_demand),
    STATUS: (_L, inp_in_status),
    PATTERNS: (_L, inp_in_pattern),  # line, fixed
    CURVES: (_L, inp_in_curve),
    CONTROLS: (_L, inp_in_control),
    RULES: (_L, inp_in_rule),
    ENERGY: (_L, inp_in_energy),
    EMITTERS: (_L, inp_in_emitter),
    QUALITY: (_L, inp_in_quality),
    SOURCES: (_L, inp_in_source),
    REACTIONS: (_L, inp_in_reaction),
    MIXING: (_L, inp_in_mixing),
    TIMES: (_S, inp_in_time),
    REPORT: (_S, inp_in_report),
    OPTIONS: (_S, _inp_in_option),  # line, version
    COORDINATES: (_L, inp_in_coord),
    VERTICES: (_L, inp_in_vertex),
    REGION: (_L, inp_in_region),
    BOUND: (_L, inp_in_bound),
    REGION_NODES: (_L, inp_in_regionnodes),
    LABELS: (_L, inp_in_label),
    BACKDROP: (_S, inp_in_backdrop),
    # END         : 'END',
}

_level_1 = [
    TITLE,
    PATTERNS,
    CURVES,
    CONTROLS,
    RULES,
    TIMES,
    REPORT,
    OPTIONS,
    BACKDROP,
]

_level_2 = [
    JUNCTIONS,
    RESERVOIRS,
    TANKS,
]

_level_3 = [
    PIPES,
    PUMPS,
    VALVES,
    DEMANDS,
    EMITTERS,
    QUALITY,
    SOURCES,
    MIXING,
    COORDINATES,
    LABELS,
]

_level_4 = [
    TAGS,
    STATUS,
    ENERGY,
    REACTIONS,
    VERTICES,
    REGION,
    BOUND,
    REGION_NODES,
]

map_regiontype = {
    # map the region types from desktop to server
    "DISTRIBUTION": "WDA",
    "DMA": "DMA",
    "PMA": "PMA",
    "VD": "VD",
    "SA": "SA",
}


class SQLBatch:
    def __init__(self, project: str, count: int = 100) -> None:
        self.batch: list[str] = []
        self.project = project
        self.count = count

    def add(self, sql: str) -> None:
        self.batch.append(sql)
        if len(self.batch) == self.count:
            self.flush()

    def flush(self) -> None:
        write(self.project, "".join(self.batch))
        self.batch.clear()


def _print_time(desc: str) -> datetime.datetime:
    now = datetime.datetime.now()
    time = now.strftime("%Y-%m-%d %H:%M:%S")
    print(f"{time}: {desc}")
    return now


def _get_file_offset(inp: str) -> tuple[dict[str, list[int]], bool]:
    offset: dict[str, list[int]] = {}

    current = ""
    demand_outside = False

    with open(inp) as f:
        while True:
            line = f.readline()
            if not line:
                break

            line = line.strip()
            if line.startswith("["):
                for s in section_name:
                    if line.startswith(f"[{s}"):
                        if s not in offset:
                            offset[s] = []
                        offset[s].append(f.tell())
                        current = s
                        break
            elif line != "" and line.startswith(";") == False:
                if current == DEMANDS:
                    demand_outside = True

    return (offset, demand_outside)


def parse_file(project: str, inp: str, version: str = "3") -> None:
    start = _print_time(f'Start reading file "{inp}"...')

    _print_time("First scan...")
    offset, demand_outside = _get_file_offset(inp)

    levels = _level_1 + _level_2 + _level_3 + _level_4

    # parse the whole section rather than line
    sections: dict[str, list[str]] = {}
    for [s, t] in _handler.items():
        if t[0] == _S:
            sections[s] = []

    variable_patterns = []
    current_pattern = None
    current_curve = None
    curve_type_desc_line = None
    current_region = None
    current_bound = []
    current_bound.clear()
    region_list = {}
    current_region_nodes = []
    current_region_nodes.clear()

    sql_batch = SQLBatch(project)
    _print_time("Second scan...")
    with open(inp) as f:
        for s in levels:
            if s not in offset:
                continue

            if s == DEMANDS and demand_outside == False:
                continue

            _print_time(f"[{s}]")

            is_s = _handler[s][0] == _S
            handler = _handler[s][1]

            for ptr in offset[s]:
                f.seek(ptr)

                while True:
                    line = f.readline()
                    if not line:
                        break

                    line = line.strip()
                    if line.startswith("["):
                        break
                    elif line == "":
                        continue

                    if is_s:
                        sections[s].append(line)
                    else:
                        if line.startswith(";"):
                            if version != "3":  # v2
                                line = line.removeprefix(";")
                                if s == PATTERNS:  # ;desc
                                    pass
                                elif s == CURVES:  # ;type: desc
                                    curve_type_desc_line = line
                            continue

                        if s == PATTERNS:
                            tokens = line.split()

                            if tokens[1].upper() in pattern_v3_types:  # v3
                                sql_batch.add(
                                    f"insert into _pattern (id) values ('{tokens[0]}');"
                                )
                                current_pattern = tokens[0]
                                if tokens[1].upper() == "VARIABLE":
                                    variable_patterns.append(tokens[0])
                                continue

                            if current_pattern != tokens[0]:
                                sql_batch.add(
                                    f"insert into _pattern (id) values ('{tokens[0]}');"
                                )
                                current_pattern = tokens[0]

                        elif s == CURVES:
                            tokens = line.split()

                            if tokens[1].upper() in curve_types:  # v3
                                sql_batch.add(
                                    f"insert into _curve (id, type) values ('{tokens[0]}', '{tokens[1].upper()}');"
                                )
                                current_curve = tokens[0]
                                continue

                            if current_curve != tokens[0]:
                                type = curve_types[0]
                                if curve_type_desc_line != None:
                                    type = curve_type_desc_line.split(":")[0].strip()
                                sql_batch.add(
                                    f"insert into _curve (id, type) values ('{tokens[0]}', '{type}');"
                                )
                                current_curve = tokens[0]
                                curve_type_desc_line = None
                        elif s == REGION:
                            tokens = line.split()
                            region_list[tokens[0]] = tokens[1]
                        elif s == BOUND:
                            tokens = line.split()
                            if tokens[0] != current_region and len(current_bound) > 0:
                                # insert the previous region after get all the vertex of the attatched geometry
                                current_bound.append(current_bound[0])
                                current_geometry = to_postgis_polygon(current_bound)
                                region_type = map_regiontype[region_list[tokens[0]]]
                                sql_batch.add(
                                    f"insert into region(id, boundary,r_type) values ('{current_region}', '{current_geometry}','{region_type}');"
                                )
                                # start the new region
                                current_bound.clear()
                            vertex_point = (float(tokens[1]), float(tokens[2]))
                            current_bound.append(vertex_point)
                            current_region = tokens[0]
                        elif s == REGION_NODES:
                            tokens = line.split()
                            if (
                                tokens[0] != current_region
                                and len(current_region_nodes) > 0
                            ):
                                # insert the previous region after get all the vertex of the attatched geometry
                                sql_batch.add(
                                    get_insert_into_region_sql(
                                        current_region, current_region_nodes
                                    )
                                )
                                # start the new region
                                current_region_nodes.clear()
                            current_region_nodes.append(tokens[1])
                            current_region = tokens[0]
                        if s == JUNCTIONS:
                            sql_batch.add(handler(line, demand_outside))
                        elif s == PATTERNS:
                            sql_batch.add(
                                handler(line, current_pattern not in variable_patterns)
                            )
                        elif s == BOUND or s == REGION_NODES:
                            continue
                        else:
                            sql_batch.add(handler(line))

                f.seek(0)

            if is_s:
                if s == OPTIONS:
                    sql_batch.add(handler(sections[s], version))
                else:
                    sql_batch.add(handler(sections[s]))
        # need to insert the last region into database
        if len(current_bound) > 0:
            current_bound.append(current_bound[0])
            current_geometry = to_postgis_polygon(current_bound)
            region_type = map_regiontype[region_list[current_region]]
            sql_batch.add(
                f"insert into region(id, boundary,r_type) values ('{current_region}', '{current_geometry}','{region_type}');"
            )
            # reset the current region to none for the [REGION_NODES] session reading
            # current_region=None
        # need to insert the  last region_nodes into database
        if len(current_region_nodes) > 0:
            sql_batch.add(
                get_insert_into_region_sql(current_region, current_region_nodes)
            )
            # current_region=None
        sql_batch.flush()

    end = _print_time(f'End reading file "{inp}"')
    print(f"Total (in second): {(end-start).seconds}(s)")


def get_insert_into_region_sql(region: str, nodes: list[str]) -> str:
    str_sql = ""
    str_nodes = str(nodes).replace("'", "''")
    r_type = region[0 : region.index("_")]
    if r_type == "DMA" or r_type == "SA" or r_type == "VD":
        table = ""
        if r_type == "DMA":
            table = "region_dma"
        elif r_type == "SA":
            table = "region_sa"
            source = region[region.index("_") + 1 :]
            str_sql = f"insert into region_sa(id,time_index,source,nodes) values ('{region}', 0,'{source}','{str_nodes}');"
        elif r_type == "VD":
            table = "region_vd"

    return str_sql


def read_inp(project: str, inp: str, version: str = "3") -> bool:
    if version != "3" and version != "2":
        version = "2"

    if is_project_open(project):
        close_project(project)

    if have_project(project):
        delete_project(project)

    create_project(project)
    open_project(project)

    parse_file(project, inp, version)

    """try:
        parse_file(project, inp, version)
    except:
        close_project(project)
        delete_project(project)
        return False"""

    close_project(project)
    return True


# DingZQ, 2024-12-28, convert v3 to v2
def convert_inp_v3_to_v2(inp: str) -> ChangeSet:
    project = "v3Tov2"

    if is_project_open(project):
        close_project(project)

    if have_project(project):
        delete_project(project)

    create_project(project)
    open_project(project)

    filename = f"inp/{project}_temp.inp"
    if os.path.exists(filename):
        os.remove(filename)

    with open(filename, "w") as f:
        f.write(inp)

    parse_file(project, filename, "3")

    """try:
        parse_file(project, inp, version)
    except:
        close_project(project)
        delete_project(project)
        return False"""

    return export_inp(project, "2")


def import_inp(project: str, cs: ChangeSet, version: str = "3") -> bool:
    if version != "3" and version != "2":
        version = "2"

    if "inp" not in cs.operations[0]:
        return False

    filename = f"inp/{project}_temp.inp"
    if os.path.exists(filename):
        os.remove(filename)

    _print_time(f'Start writing temp file "{filename}"...')
    with open(filename, "w", encoding="GBK") as f:
        f.write(str(cs.operations[0]["inp"]))
    _print_time(f'End writing temp file "{filename}"...')

    result = read_inp(project, filename, version)

    # os.remove(filename)

    return result
