import shutil
import subprocess
import typing
from queue import Queue, Empty
from threading import Thread
from tkinter import END, DISABLED, NORMAL, ttk
from tkinter import Toplevel, Text


class TaskProcessManager(object):
    def __init__(
            self, title_name: str,
            task_done_trigger_function: typing.Callable = None,
            error_trigger_function: typing.Callable = None,
            use_theme=None
    ):
        super().__init__()
        # self param
        self.read_program_error_output_from_thread = None
        self.read_program_output_from_thread = None
        self.still_run_program = True
        self.program_encoding = "utf-8"
        self.run_output_queue = Queue()
        self.run_error_queue = Queue()
        self.process = None
        self.task_done_trigger_function = task_done_trigger_function
        self.error_trigger_function = error_trigger_function
        # ui
        self.title_name = title_name
        self.tkinter_top_level = None
        self.tkinter_text_frame = None
        self.tkinter_text = None
        self.tkinter_text_scrollbar_y = None
        self.tkinter_text_scrollbar_x = None
        self.style = ttk.Style()
        if use_theme is not None:
            self.style.theme_use(use_theme)

    def set_ui(self):
        # self tkinter ui
        self.tkinter_top_level = Toplevel()
        self.tkinter_text_frame = ttk.Frame(self.tkinter_top_level, padding="3 3 12 12")
        self.tkinter_text = Text(self.tkinter_text_frame, wrap="none")
        self.tkinter_text_scrollbar_y = ttk.Scrollbar(self.tkinter_text_frame, orient="vertical",
                                                      command=self.tkinter_text.yview)
        self.tkinter_text_scrollbar_x = ttk.Scrollbar(self.tkinter_text_frame, orient="horizontal",
                                                      command=self.tkinter_text.xview)
        self.tkinter_text["yscrollcommand"] = self.tkinter_text_scrollbar_y.set
        self.tkinter_text["xscrollcommand"] = self.tkinter_text_scrollbar_x.set
        self.tkinter_text.configure(state=DISABLED)
        self.tkinter_top_level.title(self.title_name)
        self.tkinter_text_frame.grid(column=0, row=0, sticky="nsew")
        self.tkinter_text.grid(column=0, row=0, sticky="nsew")
        self.tkinter_text_scrollbar_y.grid(column=1, row=0, sticky="ns")
        self.tkinter_text_scrollbar_x.grid(column=0, row=1, sticky="nsew")
        self.tkinter_text.tag_configure("warning", foreground="red")
        self.tkinter_text.bind("<1>", lambda event: self.tkinter_text.focus_set())
        self.tkinter_text_frame.columnconfigure(0, weight=1)
        self.tkinter_text_frame.rowconfigure(0, weight=1)
        self.tkinter_top_level.columnconfigure(0, weight=1)
        self.tkinter_top_level.rowconfigure(0, weight=1)

    def check_return_code(self):
        try:
            self.tkinter_text.configure(state=NORMAL)
            if not self.run_output_queue.empty():
                self.tkinter_text.insert(END, self.run_output_queue.get_nowait() + "\n")
            if not self.run_error_queue.empty():
                self.tkinter_text.insert(END, self.run_error_queue.get_nowait(), "warning")
            self.tkinter_text.configure(state=DISABLED)
        except Empty:
            pass
        process_return_code = self.process.poll()
        if process_return_code is None:
            self.tkinter_text.after(10, self.check_return_code)
        elif process_return_code == 0:
            self.still_run_program = False
            self.print_and_clear_queue()
            self.process = None
            if self.task_done_trigger_function is not None:
                self.task_done_trigger_function()
        else:
            self.still_run_program = False
            self.print_and_clear_queue()
            self.process = None
            if self.error_trigger_function is not None:
                self.error_trigger_function()

    def start_test_process(self, package: str, exec_str: str):
        # try to find file and compiler
        compiler_path = shutil.which("python")
        if compiler_path is None:
            compiler_path = shutil.which("python3")
        self.set_ui()
        self.process = subprocess.Popen(
            [
                compiler_path,
                "-m",
                package,
                "--execute_str",
                exec_str
            ],
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE
        )
        self.still_run_program = True
        # program output message queue thread
        self.read_program_output_from_thread = Thread(
            target=self.read_program_output_from_process,
            daemon=True
        ).start()
        # program error message queue thread
        self.read_program_error_output_from_thread = Thread(
            target=self.read_program_error_output_from_process,
            daemon=True
        ).start()
        self.check_return_code()

    def print_and_clear_queue(self):
        try:
            self.tkinter_text.configure(state=NORMAL)
            for std_output in iter(self.run_output_queue.get_nowait, None):
                self.tkinter_text.insert(END, std_output + "\n")
            for std_err in iter(self.run_error_queue.get_nowait, None):
                self.tkinter_text.insert(END, std_err, "warning", "\n")
            self.tkinter_text.configure(state=DISABLED)
        except Empty:
            pass
        self.run_output_queue = Queue()
        self.run_error_queue = Queue()

    def read_program_output_from_process(self):
        while self.still_run_program:
            program_output_data = self.process.stdout.raw.read(1024000).decode(self.program_encoding)
            if program_output_data.strip() != "":
                self.run_output_queue.put(program_output_data)

    def read_program_error_output_from_process(self):
        while self.still_run_program:
            program_error_output_data = self.process.stderr.raw.read(1024000).decode(self.program_encoding)
            if program_error_output_data.strip() != "":
                self.run_error_queue.put(program_error_output_data)
