import math
import os

from tko.util.identifier import Identifier
from tko.enums.identifier_type import IdentifierType
from tko.run.unit import Unit
from tko.util.param import Param
from tko.run.loader import Loader
from tko.run.solver_builder import SolverBuilder
from tko.util.text import Text
from tko.util.symbols import symbols
from tko.util.label_factory import LabelFactory
from tko.down.drafts import Drafts
from pathlib import Path
from tko.settings.settings import Settings

class Wdir:
    def __init__(self, settings: Settings):
        self.settings = settings
        self.__autoload = False
        self.__autoload_folder: Path | None = None
        self.__solver: None | SolverBuilder = None
        self.__source_list: list[Path] = []
        self.__pack_list: list[list[Unit]] = []
        self.__unit_list: list[Unit] = []
        self.__curses = False
        self.__lang = ""

    def has_solver(self) -> bool:
        return not self.__solver is None

    def has_tests(self) -> bool:
        return len(self.__unit_list) != 0

    def get_solver(self) -> SolverBuilder:
        if self.__solver is None:
            raise Warning("fail: Não foi encontrado arquivo de código")
        return self.__solver
    
    def get_unit_list(self) -> list[Unit]:
        return self.__unit_list

    def get_unit(self, index: int) -> Unit:
        return self.__unit_list[index]
    
    def get_source_list(self) -> list[Path]:
        return self.__source_list

    def set_curses(self, value: bool):
        self.__curses = value
        return self

    def set_lang(self, lang: str):
        self.__lang = lang
        return self
    
    def is_curses(self) -> bool:
        return self.__curses

    def is_autoload(self) -> bool:
        return self.__autoload

    def get_autoload_folder(self) -> Path | None:
        return self.__autoload_folder

    def set_solver(self, solver_list: list[Path]):
        if len(solver_list) > 0:
            self.__solver = SolverBuilder(solver_list, self.settings)
        return self

    def set_sources(self, source_list: list[Path]):
        self.__source_list = source_list
        return self

    def autoload(self):
        if self.__autoload_folder is None:
            raise Warning("fail: pasta de autoload não definida")
        folder: Path = self.__autoload_folder

        # loading source list
        source_list: list[Path] = [target for target in folder.iterdir() if target.suffix in [".tio", ".vpl", ".toml"]]
        source_list.extend([target for target in folder.iterdir() if target.suffix == ".md"])
        
        if self.__lang != "":
            solver_list = Drafts.load_drafts_only(folder, self.__lang)
        else:
            solver_list = Drafts.load_drafts_only(folder, "", extra=Drafts().get_languages_with_drafts())
        solver_list = sorted(solver_list)

        self.set_solver(solver_list)
        self.set_sources(source_list)
        self.__autoload = True
        return self

    def set_target_list(self, target_list: list[Path]):
        if len(target_list) == 0:
            target_list.append(Path())
        if len(target_list) == 1 and os.path.isdir(target_list[0]):
            self.__autoload_folder = target_list[0]
            return self.autoload()
                    
        for target in target_list:
            if not os.path.exists(target):
                raise Warning(f"fail: {target} não encontrado")

        solvers = [target for target in target_list if Identifier.get_type(target.suffix) == IdentifierType.SOLVER]
        sources = [target for target in target_list if Identifier.get_type(target.suffix) != IdentifierType.SOLVER]

        self.set_solver(solvers)
        self.set_sources(sources)
        return self

    def build(self):
        loading_failures = 0
        self.__pack_list = []
        for source in self.__source_list:
            try:
                raw_list: list[Unit] = Loader.parse_source(source)
                unit_list: list[Unit] = []
                for unit in raw_list:
                    if unit.get_expected() == "" and unit.get_input() == "":
                        continue
                    unit_list.append(unit)
                self.__pack_list.append(unit_list)
            except FileNotFoundError as e:
                print(str(e))
                loading_failures += 1
                pass
        if loading_failures > 0 and loading_failures == len(self.__source_list):
            raise FileNotFoundError("failure: nenhum arquivo de teste encontrado")
        self.__unit_list = []
        input_dict: dict[str, int] = {}
        index = 0
        for pack in self.__pack_list:
            for unit in pack:
                if unit.get_input() in input_dict:
                    unit.repeated = input_dict[unit.get_input()]
                else:
                    input_dict[unit.get_input()] = index
                    unit.index = index
                    self.__unit_list.append(unit)
                    index += 1
        #self.__number_and_mark_duplicated()
        #self.__remove_duplicated()
        self.__calculate_grade()
        self.__pad()
        return self

    def calc_grade(self) -> int:
        grade = 100
        for case in self.__unit_list:
            if not case.repeated and (case.get_received() is None or case.get_expected() != case.get_received()):
                grade -= case.grade_reduction
        return max(0, grade)

    # put all the labels with the same length
    def __pad(self):
        if len(self.__unit_list) == 0:
            return
        max_case = max([len(x.case) for x in self.__unit_list])
        max_source = max([len(x.source) for x in self.__unit_list])
        for unit in self.__unit_list:
            unit.case_pad = max_case
            unit.source_pad = max_source

    # select a single unit to execute exclusively
    def filter(self, param: Param.Basic):
        index = param.index
        if index is not None:
            if 0 <= index < len(self.__unit_list):
                self.__unit_list = [self.__unit_list[index]]
            else:
                raise ValueError("Índice fora dos limites: " + str(index))
        return self

    # calculate the grade reduction for the cases without grade
    # the grade is proportional to the number of unique cases
    def __calculate_grade(self):
        unique_count = len([x for x in self.__unit_list if not x.repeated])
        for unit in self.__unit_list:
            if unit.grade is None:
                unit.grade_reduction = math.floor(100 / unique_count)
            else:
                unit.grade_reduction = unit.grade

    # number the cases and mark the repeated
    # def __number_and_mark_duplicated(self):
    #     new_list: list[Unit] = []
    #     index = 0
    #     for unit in self.__unit_list:
    #         unit.index = index
    #         index += 1
    #         search = [x for x in new_list if x.input == unit.input]
    #         if len(search) > 0:
    #             unit.repeated = search[0].index
    #         new_list.append(unit)
    #     self.__unit_list = new_list

    # def __remove_duplicated(self):
    #     self.__unit_list = [unit for unit in self.__unit_list if unit.repeated is None]

    # sort, unlabel ou rename using the param received
    def manipulate(self, param: Param.Manip):
        # filtering marked repeated
        self.__unit_list = [unit for unit in self.__unit_list if unit.repeated is None]
        if param.to_sort:
            self.__unit_list.sort(key=lambda v: len(v.input))
        if param.unlabel:
            for unit in self.__unit_list:
                unit.case = ""
        if param.to_number:
            number = 00
            for unit in self.__unit_list:
                unit.case = LabelFactory().label(unit.case).index(number).generate()
                number += 1

    def unit_list_resume(self) -> list[Text]:
        return [unit.str() for unit in self.__unit_list]

    def sources_names(self) -> list[tuple[str, str]]:
        out: list[tuple[str, str]] = []
        if len(self.__pack_list) == 0:
            out.append((symbols.failure.text, "0"))
        for source_name, ulist in zip(self.__source_list, self.__pack_list):
            nome: str = source_name.name
            count = len([unit for unit in ulist if unit.repeated is None])
            if count > 0:
                count_str = str(count)
                total = len(ulist)
                if count < total:
                    count_str += "/" + str(total)
                out.append((nome, count_str))
        return out

    def solvers_names(self) -> list[str]:
        path_list = [] if self.__solver is None else self.__solver.args_list
        if self.__solver is not None and len(path_list) == 0:  # free_cmd
            out = ["free cmd"]
        else:
            out = [os.path.basename(path) for path in path_list]
        return out

    def resume_splitted(self) -> Text:
        sources = ["{}({})".format(name, str(count).rjust(2, "0")) for name, count in self.sources_names()]
        __sources = Text().add("Testes:").add("[").addf("y", ", ".join(sources)).add("]")
        __solvers = Text().add("Códigos:").add("[").addf("g", ", ".join(self.solvers_names())).add("]")
        return Text().add(__solvers).add(" ").add(__sources)

    def resume_join(self) -> Text:
        sources = ["{}({})".format(name, str(count).rjust(2, "0")) for name, count in self.sources_names()]
        __sources = Text().addf("y", ", ".join(sources))
        __solvers = Text().addf("g", ", ".join(self.solvers_names()))
        return Text().add(__solvers).add(", ").add(__sources)