import curses
from .play.fmt import Fmt
from .run.basic import ExecutionResult
from .run.unit import Unit
from .run.param import Param
from .execution import Execution
from .run.diff import Diff
from .util.sentence import Sentence, Token, RToken
from typing import List, Tuple, Optional
from .play.frame import Frame
from .play.floating import Floating
from .play.floating_manager import FloatingManager
from .play.images import images, compilling, success, intro, executing
from .util.runner import Runner
from .util.freerun import Free
from .play.style import Style
from .game.task import Task
import random
from .play.opener import Opener
from .run.solver import CompileError

from .run.wdir import Wdir
from .run.report import Report
from .settings.settings_parser import SettingsParser
import os
from .run.basic import Success
import enum

class Mode(enum.Enum):
    intro = 0
    select = 1
    running = 2
    finished = 3

class Keys:
    left = "a"
    up = "w"
    right = "d"
    down = "s"
    principal = "p"
    diff = "m"
    rodar = "r"
    testar = "t"
    testar2 = "\n"
    sair = "q"
    editar = "e"
    bloquear = "b"

class CDiff:
    def __init__(self, wdir: Wdir, param: Param.Basic, success_type: Success):
        self.param = param
        self.results: List[Tuple[ExecutionResult, int]] = []
        self.wdir = wdir
        self.unit_list = [unit for unit in wdir.get_unit_list()] # unit list to be consumed
        self.exit = False
        self.success_type = success_type
        self.task = Task()
        self.init = 0   # index of first line to show
        self.length = 1  # length of diff
        self.space = 0  # dy space for draw
        self.mode: Mode = Mode.intro

        self.locked_index: bool = False

        self.focused_index = 0
        self.finished = False
        self.resumes: List[str] = []

        self.sp = SettingsParser()
        self.settings = self.sp.load_settings()
        self.colors = self.settings.geral.is_colored()
        self.first_loop = True
        self.fman = FloatingManager()
        self.first_run = False

        self.opener: Optional[Opener] = None

    def set_first_run(self):
        self.first_run = True
        return self

    def set_opener(self, opener: Opener):
        self.opener = opener
        self.opener.set_fman(self.fman)
        return self

    def save_settings(self):
        self.settings.geral.set_is_diff_down(self.param.is_up_down)
        self.sp.save_settings()
        return self
    
    def set_task(self, task: Task):
        self.task = task
        return self

    def set_exit(self):
        self.exit = True
        return self

    def print_centered_image(self, image: str, color: str, clear=False, align: str = "."):
        dy, dx = Fmt.get_size()
        lines = image.split("\n")[1:]
        init_y = 4
        if align == "v":
            init_y = dy - len(lines) - 1
        for i, line in enumerate(lines):
            info = Sentence().addf(color, line).center(dx - 2, Token(" ", " "))
            if clear:
                Fmt.write(i + init_y, 1, Sentence(" " * info.len()))
            else:
                Fmt.write(i + init_y, 1, Sentence().addf(color, line).center(dx - 2, Token(" ", " ")))


    def random_get(self, dic: dict, mode:str = "static"):
        if mode == "static":
            count = sum([ord(c) for c in self.get_folder()])
            keys = list(dic.keys())
            return dic[keys[count % len(keys)]]
        else:
            keys = list(dic.keys())
            return dic[random.choice(keys)]

    def show_success(self):
        if self.success_type == Success.RANDOM:
            out = self.random_get(images, "static")
        else:
            out = self.random_get(success, "static")
        self.print_centered_image(out, "" if not self.colors else "g")
        
    def show_compilling(self, clear=False):
        out = self.random_get(compilling, "random")
        self.print_centered_image(out, "" if not self.colors else "y", clear)

    def show_executing(self, clear=False):
        out = executing
        self.print_centered_image(out, "" if not self.colors else "y", clear, "v")

    def draw_scrollbar(self):
        y_init = 3
        # if len(self.results_fail) == 0:
        #     return
        tr = "╮"
        br = "╯"
        vbar = "│"
        bar = []

        if self.length > self.space:
            total = self.space
            _begin = False
            _end = False
            if self.init == 0:
                _begin = True
            if self.init == self.length - self.space:
                _end = True

            pre = int((self.init / self.length) * total)
            mid = int((self.space / self.length) * total)
            pos = (max(0, total - pre - mid))

            if _begin:
                pre -= 1
            if _end:
                pos -= 1

            if self.init > 0 and pre == 0:
                pre = 1
                pos -= 1

            if _begin:
                bar.append(tr)
            for _ in range(pre):
                bar.append(vbar)
            for _ in range(mid):
                bar.append("┃")
            for _ in range(pos):
                bar.append(vbar)
            if _end:
                bar.append(br)

        else:
            bar.append(tr)
            for i in range(self.length - 2):
                bar.append(vbar)
            bar.append(br)


        _, cols = Fmt.get_size()
        for i in range(len(bar)):
            Fmt.write(i + y_init, cols - 1, Sentence().add(bar[i]))

    def get_folder(self):
        source_list = self.wdir.get_source_list()
        if source_list:
            folder = os.path.abspath(source_list[0])
        else:
            folder = os.path.abspath(self.wdir.get_solver().path_list[0])
        return folder.split(os.sep)[-2]

    def get_focused_unit(self) -> Unit:
        if len(self.results) != 0:
            _, index = self.results[self.focused_index]
            unit = self.wdir.get_unit(index)
            return unit
        return self.wdir.get_unit(self.focused_index)

    def get_token(self, result: ExecutionResult) -> Token:
        if result == ExecutionResult.SUCCESS:
            return Token(ExecutionResult.get_symbol(ExecutionResult.SUCCESS).text, "G")
        elif result == ExecutionResult.WRONG_OUTPUT:
            return Token(ExecutionResult.get_symbol(ExecutionResult.WRONG_OUTPUT).text, "R")
        elif result == ExecutionResult.COMPILATION_ERROR:
            return Token(ExecutionResult.get_symbol(ExecutionResult.COMPILATION_ERROR).text, "Y")
        elif result == ExecutionResult.EXECUTION_ERROR:
            return Token(ExecutionResult.get_symbol(ExecutionResult.EXECUTION_ERROR).text, "M")
        else:
            return Token(ExecutionResult.get_symbol(ExecutionResult.UNTESTED).text, "W")

    def process_one(self):

        if self.mode != Mode.running:
            return

        solver = self.wdir.get_solver()

        if solver.compile_error:
            self.mode = Mode.finished
            while len(self.unit_list) > 0:
                index = len(self.results)
                self.unit_list = self.unit_list[1:]
                self.results.append((ExecutionResult.COMPILATION_ERROR, index))
            return
        
        if self.locked_index:
            self.mode = Mode.finished
            unit = self.get_focused_unit()
            unit.result = Execution.run_unit(solver, unit)
            return

        if len(self.unit_list) > 0:
            index = len(self.results)
            unit = self.unit_list[0]
            self.unit_list = self.unit_list[1:]
            unit.result = Execution.run_unit(solver, unit)
            self.results.append((unit.result, index))
            self.task.test_progress = (len(self.results) * 100) // len(self.wdir.get_unit_list())
            self.focused_index = index

        if len(self.unit_list) == 0:
            self.mode = Mode.finished
            self.focused_index = 0


            done_list: List[Tuple[ExecutionResult, int]] = []
            fail_list: List[Tuple[ExecutionResult, int]] = []
            for data in self.results:
                unit_result, _ = data
                if unit_result != ExecutionResult.SUCCESS:
                    fail_list.append(data)
                else:
                    done_list.append(data)
            self.results = fail_list + done_list


    def draw_top_line(self):
        # construir mais uma solução
        activity_color = "W" if not self.colors else "C"
        solver_color = "M" if not self.colors else "M"
        sources_color = "W" if not self.colors else "Y"
        running_color = "W" if not self.colors else "R"

        _, cols = Fmt.get_size()
        frame = Frame(0, 0).set_size(3, cols)

        folder = self.get_folder()
        activity = Sentence().addf(activity_color, folder)
        solvers = Sentence()
        for i, solver in enumerate(self.get_solver_names()):
            # if i != 0:
            #     solvers.addf(solver_color, ", ")
            color = solver_color
            if i == self.task.main_index:
                color = "G"
            solvers.addf(color.lower(), Style.roundL()).addf(color, solver).addf(color.lower(), Style.roundR())

        sources = Sentence()
        for i, (source, _) in enumerate(self.wdir.sources_names()):
            if i != 0:
                sources.add(", ")
            sources.addf(sources_color, source)

        done = len(self.results)
        full = len(self.wdir.get_unit_list())
        sources.addf(sources_color, f"({full})")
        # solvers = Sentence().addf(solver_color.lower(), Style.roundL()).add(solvers).addf(solver_color.lower(), Style.roundR())
        activity = Sentence().addf(activity_color.lower(), Style.roundL()).add(activity).addf(activity_color.lower(), Style.roundR())
        if done != full:
            activity.addf(running_color.lower(), Style.roundL()).addf(running_color, f"({done}/{full})").addf(running_color.lower(), Style.roundR())
        sources = Sentence().addf(sources_color.lower(), Style.roundL()).add(sources).addf(sources_color.lower(), Style.roundR())

        delta = frame.get_dx() - solvers.len()
        left = 1
        right = 1
        if delta > 0:
            delta_left = delta // 2
            left = max(1, delta_left - activity.len())
            delta_right = delta - delta_left
            right = max(1, delta_right - sources.len())

        header = Sentence().add(activity).add("─" * left).add(solvers).add("─" * right).add(sources)

        frame.set_header(header)

        value = self.get_focused_unit()
        if value is not None and not self.wdir.get_solver().compile_error and not self.is_all_right():
            frame.write(0, 0, Sentence().add(value.str(False)).center(frame.get_dx()))

        output = Sentence()
        done_list = self.results
        if len(done_list) > 0 and self.locked_index:
            _, index = done_list[self.focused_index]
            done_list[self.focused_index] = (self.get_focused_unit().result, index)
        todo_list: List[Tuple[ExecutionResult, int]] = []
        i = len(done_list)
        for _ in self.unit_list:
            if self.locked_index and i == self.focused_index:
                todo_list.append((self.wdir.get_unit(self.focused_index).result, i))
            else:
                todo_list.append((ExecutionResult.UNTESTED, i))
            i += 1

        i = 0
        for unit_result, index in done_list + todo_list:
            foco = i == self.focused_index
            token = self.get_token(unit_result)
            extrap = Token(Style.roundL(), token.fmt.lower())# if not foco else Token(Style.roundL(), "")
            extras = Token(Style.roundR(), token.fmt.lower())# if not foco else Token(Style.roundR(), "")
            if foco and not self.wdir.get_solver().compile_error and not self.mode == Mode.intro and not self.is_all_right():
                if self.locked_index:
                    token.fmt = "r"
                else:
                    token.fmt = ""
            output.add(extrap).addf(token.fmt, str(index).zfill(2)).add(token).add(extras).add(" ")
            i += 1

        size = 6
        if self.focused_index * size > frame.get_dx():
            output.cut_begin((self.focused_index + 1) * size - frame.get_dx())

        frame.set_footer(output, "")
        frame.draw()
        
    def two_column_mode(self):
        _, cols = Fmt.get_size()
        return cols < Sentence(" ").join(self.make_bottom_line()).len() + 2

    def make_bottom_line(self) -> List[Sentence]:
        tokens = [
            RToken("C", "Sair[q]"),
            # RToken("C", "Navegar[wasd]"),
        ]
        if self.opener is not None:
            tokens.append(RToken("C", "Editar[e]"))
        tokens.append(RToken("G", "Testar[↲]"))
        tokens.append(RToken("M", "Rodar[r]"))
        tokens.append(RToken("M", "Principal[p]"))
        if self.settings.geral.is_diff_down():
            tokens.append(RToken("M", "[m] Ver╾Hor")) 
        else:
            tokens.append(RToken("M", "[m] Ver╼Hor"))

        cmds = []
        for t in tokens:
            color = "W" if not self.colors else t.fmt
            cmds.append(Sentence().addf(color.lower(), Style.roundL()).addf(color, t.text).addf(color.lower(), Style.roundR()))

        if self.locked_index:
            cmds.append(Sentence().addf("g", Style.sharpL()).add(RToken("G", "Bloquear[b]")).addf("g", Style.sharpR()))
        else:
            cmds.append(Sentence().addf("m", Style.sharpL()).add(RToken("M", "Bloquear[b]")).addf("m", Style.sharpR()))
        

        return cmds

    def draw_bottom_line(self):
        lines, cols = Fmt.get_size()
        if self.two_column_mode():
            half = len(self.make_bottom_line()) // 2
            if len(self.make_bottom_line()) % 2 == 1:
                half += 1
            line = self.make_bottom_line()
            one = line[:half]
            two = line[half:]
            Fmt.write(lines - 1, 0, Sentence(" ").join(one).center(cols, Token(" ")))
            Fmt.write(lines - 2, 0, Sentence(" ").join(two).center(cols, Token(" ")))
        else:
            Fmt.write(lines - 1, 0, Sentence(" ").join(self.make_bottom_line()).center(cols, Token(" ")))
 
    def is_all_right(self):
        if self.locked_index or len(self.results) == 0:
            return False
        if not self.mode == Mode.finished:
            return False
        for result, _ in self.results:
            if result != ExecutionResult.SUCCESS:
                return False
        return True

    def draw_main(self, unit: Unit):
        lines, cols = Fmt.get_size()
        self.space = lines - 4
        if self.two_column_mode():
            self.space = lines - 5
        frame = Frame(2, -1).set_inner(self.space, cols - 1).set_border_square()

        if self.is_all_right():
            self.show_success()
            return
        Report.set_terminal_size(cols)
        
        if self.wdir.get_solver().compile_error:
            received = self.wdir.get_solver().error_msg
            line_list = [Sentence().add(line) for line in received.split("\n")]
        elif self.param.is_up_down:
            line_list = Diff.mount_up_down_diff(unit, curses=True)
        else:
            line_list = Diff.mount_side_by_side_diff(unit, curses=True)

        self.length = max(1, len(line_list))

        if self.length - self.init < self.space:
            self.init = max(0, self.length - self.space)

        if self.init >= self.length:
            self.init = self.length - 1

        if self.init < self.length:
            line_list = line_list[self.init:]
        for i, line in enumerate(line_list):
            frame.write(i, 0, Sentence().add(line))

        self.draw_scrollbar()
        return

    def load_autoload_warning(self):
        if not self.wdir.is_autoload():
            return
        warning = Floating().set_header(" Seja bem vindo ").warning()
        warning.put_text("")
        warning.put_sentence(Sentence().addf("r", "Todos") + " os arquivos de código da pasta foram carregados automaticamente")
        warning.put_text("Sempre verifique no topo da tela quais arquivos foram carregados.")
        warning.put_text("Remova ou renomeie da pasta alvo os arquivo que não quer utilizar.")
        warning.put_text("")
        warning.put_text("Você também pode escolher quais arquivos deseja executar")
        warning.put_text("navegando até a pasta de destino e executando")
        warning.put_text("o comando 'tko run' com os arquivos desejados")
        warning.put_text("")
        warning.put_sentence(Sentence().addf("c", "tko run <arquivos> cases.tio")) 
        warning.put_text("")
        warning.put_sentence(Sentence().addf("r", "Exemplo: ").addf("c", "tko run main.c lib.c cases.tio")) 
        warning.put_text("")

        self.fman.add_input(warning)

    def get_solver_names(self):
        return sorted(self.wdir.solvers_names())
    
    def main(self, scr):
        self.mode = Mode.intro
        curses.curs_set(0)  # Esconde o cursor
        Fmt.init_colors()  # Inicializa as cores
        Fmt.set_scr(scr)  # Define o scr como global
        while not self.exit:
            if self.first_loop and self.first_run:
                self.first_loop = False
                self.load_autoload_warning()
            Fmt.clear()
            if self.mode == Mode.running:
                if self.wdir.get_solver().not_compiled():
                    self.draw_top_line()
                    self.draw_bottom_line()
                    self.show_compilling()
                    Fmt.refresh()
                    try:
                        self.wdir.get_solver().prepare_exec()
                    except CompileError as e:
                        self.fman.add_input(Floating("v>").error().put_text(e.message))
                        self.mode = Mode.finished

                    Fmt.clear()
                    self.draw_top_line()
                    self.draw_bottom_line()
                    Fmt.refresh()
                self.process_one()
            self.draw_top_line()

            unit = self.get_focused_unit()
            if unit is not None and self.mode != Mode.intro:
                self.draw_main(unit)
                # if not self.wdir.get_solver().compile_error:
            elif self.mode == Mode.intro:
                self.print_centered_image(self.random_get(intro), "y")
            
            self.draw_bottom_line()

            if self.fman.has_floating():
                self.fman.draw_warnings()

            if self.mode == Mode.running:
                Fmt.refresh()
                continue

            if self.fman.has_floating():
                key = self.fman.get_input()
            else:
                key = Fmt.getch()

            fn_exec = self.process_key(key)
            if fn_exec is not None:
                return fn_exec

    def run_exec_mode(self):
        self.mode = Mode.running
        if self.wdir.is_autoload():
            self.wdir.autoload()
            self.wdir.get_solver().set_main(self.get_solver_names()[self.task.main_index])
        return lambda: Free.free_run(self.wdir.get_solver(), show_compilling=True, to_clear=True, wait_input=True)

    def run_test_mode(self):
        self.mode = Mode.running
        if self.wdir.is_autoload():
            self.wdir.autoload()
        Fmt.clear()
        self.wdir.get_solver().set_main(self.get_solver_names()[self.task.main_index]).set_executable("")
        
        if self.locked_index:
            for i in range(len(self.results)):
                _, index = self.results[i]
                self.results[i] = (ExecutionResult.UNTESTED, index)
                
        else:
            self.focused_index = 0
            self.results = []
            self.unit_list = [unit for unit in self.wdir.get_unit_list()]

    def send_char_not_found(self, key):
        self.fman.add_input(Floating("v>").error()
                    .put_text("Tecla")
                    .put_text(f"char {chr(key)}")
                    .put_text(f"code {key}")
                    .put_text("não reconhecida")
                    .put_text("")
                    )

    def go_left(self):
        if self.mode == Mode.intro:
            self.mode = Mode.select
        if self.locked_index:
            return
        self.focused_index = max(0, self.focused_index - 1)
        self.init = 0

    def go_right(self):
        if self.mode == Mode.intro:
            self.mode = Mode.select
            self.focused_index = 0
            return
        if self.locked_index:
            return
        self.focused_index = min(len(self.wdir.get_unit_list()) - 1, self.focused_index + 1)
        self.init = 0

    def go_down(self):
        if self.mode == Mode.intro:
            self.mode = Mode.select
        self.init += 1

    def go_up(self):
        if self.mode == Mode.intro:
            self.mode = Mode.select
        self.init = max(0, self.init - 1)

    def change_main(self):
        if len(self.get_solver_names()) == 1:
            self.fman.add_input(
                Floating("v>").warning()
                .put_text("Seu projeto só tem um arquivo de solução")
                .put_text("Essa funcionalidade troca qual dos arquivos")
                .put_text("de solução será o principal.")
            )
            return
        self.task.main_index = (self.task.main_index + 1) % len(self.get_solver_names())

    def process_key(self, key):
        if key == ord('q'):
            self.set_exit()
        elif key == curses.KEY_LEFT or key == ord(Keys.left):
            self.go_left()
        elif key == curses.KEY_RIGHT or key == ord(Keys.right):
            self.go_right()
        elif key == curses.KEY_DOWN or key == ord(Keys.down):
            self.go_down()
        elif key == curses.KEY_UP or key == ord(Keys.up):
            self.go_up()
        elif key == ord(Keys.diff):
            self.param.is_up_down = not self.param.is_up_down
            self.save_settings()
            self.init = 0
        elif key == ord(Keys.principal):
            self.change_main()
        elif key == ord(Keys.rodar):
            return self.run_exec_mode()
        elif key == ord(Keys.testar) or key == ord(Keys.testar2):
            self.run_test_mode()
        elif key == ord(Keys.bloquear):
            self.locked_index = not self.locked_index
            if self.mode == Mode.intro:
                self.mode = Mode.select
        elif key == ord(Keys.editar):
            if self.opener is not None:
                self.opener.open_code(open_dir=True)
        elif key != -1 and key != curses.KEY_RESIZE:
            self.send_char_not_found(key)

    def run(self):
        while True:
            free_run_fn = curses.wrapper(self.main)
            if free_run_fn == None:
                break
            else:
                while(free_run_fn()):
                    pass
