import os
import sys
import subprocess

cwd = __file__.rsplit("\\", 1)[0]
subprocess.check_call([sys.executable] + "-m pip install -r".split(' ') + [f"{cwd}/requirements.txt"])
os.system("cls")

import ctypes
import tkinter as tk
import importlib.util
from globals import *
from tkinter import font
from pathlib import Path
from PIL import Image, ImageTk
from tkinterdnd2 import TkinterDnD

sync_on = False
try:
    import AppSync
    sync_on = True
except:
    pass

SERVER_URL = os.getenv("SERVER_URL")
APPS_FOLDER = Path(cwd) / "apps"
ASSET_FOLDER = Path(cwd).parent / "Assets"

MIN_WINDOW_SIZE = (600,600)
MAX_WINDOW_SIZE = (1920,1080)
CANVAS_SIZE = (800,800)

kernel32 = ctypes.WinDLL("kernel32")
user32 = ctypes.WinDLL("user32")

class Dashboard(TkinterDnD.Tk):
    def __init__(self):
        super().__init__()
        self.font_10 = font.Font(family=FONT_FAM, size=10)
        self.font_10_bold = font.Font(family=FONT_FAM, size=10, weight="bold")
        self.font_8 = font.Font(family=FONT_FAM, size=8)
        self.font_8_bold = font.Font(family=FONT_FAM, size=8, weight="bold")
        self.console_font_7 = font.Font(family='Consolas', size=7)
        self.console_font_7_bold = font.Font(family='Consolas', size=7, weight="bold")
        self.option_add("*Font", self.font_10_bold)
        self.resizable(False, False)
        self.iconbitmap(ASSET_FOLDER / "MudaeAvatar.ico")
        self.title(f"Mudae Apps{'' if sync_on else ' -dev'}")
        self.update()
        self.hide_console() if sync_on else self.show_console()
        self.set_geometry(x_size=0, y_size=0, x_pos=100, y_pos=100)
        self.update_idletasks()
        self.dark_mode()
        self.build_dash()
        self.update()
        self.find_apps()
        self.dash_to_front()
        self.build_ui(self.canvas)
    
    def build_dash(self):
        def _on_canvas_config(event):
            if (event.width == CANVAS_SIZE[0]) and (event.height == CANVAS_SIZE[1]): return
            new_w = CANVAS_SIZE[0] + (self.winfo_width() - event.width)
            new_h = CANVAS_SIZE[1] + (self.winfo_height() - event.height)
            self.set_geometry(x_size=new_w, y_size=new_h)
        def _on_mouse_move(event):
            if self.dash_after:
                self.after_cancel(self.dash_after)
                self.dash_after = None
        def _on_cmd(event):
            if self.console_visibility_state():
                return self.hide_console()
            self.show_console()

        self.get_images()
        self.dash_after = None
        self.side_bar = tk.Frame(self, bg=BG_COL, relief='flat', bd=0, width=20)
        self.side_bar.pack(fill=tk.Y, side="left")
        self.nav_bar = tk.Frame(self, bg=BG_COL, relief='flat', bd=0, height=20)
        self.nav_bar.pack(fill=tk.X, side="top")
        self.tips_bar = tk.Frame(self, bg=BG_COL, relief='flat', bd=0, height=20)
        self.tips_bar.pack(fill=tk.X, side="bottom")
        self.title_label = tk.Label(self.side_bar, text="App Dashboard", 
                                    bg=BG_COL, fg=L_GREY)
        self.title_label.pack(side='top', padx=10)
        
        self.set_logger()
        
        self.mouse_pos_text = tk.StringVar(value="(0, 0)")
        self.mouse_pos_Label = tk.Canvas(self.tips_bar, width=50, height=20, highlightthickness=0, bg=BG_COL)
        self.mouse_pos_Label.pack(side='left')
        self.mouse_pos_Label.create_window(25, 10,window=tk.Label(textvariable=self.mouse_pos_text, anchor="center", bg=BG_COL, fg=L_GREY))
        
        self.cmd_Label = tk.Canvas(self.tips_bar, width=16, height=12, highlightthickness=0, bg=D_GREY)
        self.cmd_Label.pack(side='right', padx=4, pady=4)
        self.cmd_Label.create_image((0, 0), image=self.outline_16x12_TK, anchor='nw')
        self.cmd_Label.create_text((8, 5), text='>_', font=self.console_font_7_bold, fill=L_GREY, anchor='center')
        
        self.cmd_Label.bind('<Button-1>', _on_cmd)
        
        self.canvas = tk.Canvas(self, bg=D_GREY, highlightthickness=0, width=CANVAS_SIZE[0], height=CANVAS_SIZE[1])
        self.canvas.pack()
        self.canvas.bind('<Configure>', _on_canvas_config)
        self.canvas.bind_all("<Motion>", _on_mouse_move)
        
        self.arch_id_NW = self.canvas.create_image(0,0,image=self.arch_10x10_TK,anchor="nw", tags=("dash_overlay", "keep"))
        self.arch_id_SW = self.canvas.create_image(0,CANVAS_SIZE[1],image=self.arch_10x10_F_TK,anchor="sw", tags=("dash_overlay", "keep"))
    
    def build_ui(self, ui:tk.Canvas):
        for app in self.apps:
            if app.name == DEFAULT_APP:
                app.launch(ui)
                return
        self.apps[0].launch(ui)
    
    def console_visibility_state(self):
        hwnd = kernel32.GetConsoleWindow()
        if not hwnd:
            return 0
        return 1 if user32.IsWindowVisible(hwnd) else 0
    
    def hide_console(self):
        hwnd = kernel32.GetConsoleWindow()
        if hwnd:
            user32.ShowWindow(hwnd, 0)
    
    def show_console(self):
        hwnd = kernel32.GetConsoleWindow()
        if hwnd:
            user32.ShowWindow(hwnd, 9)
    
    def set_logger(self):
        class _DashboardLogger():
            def __init__(self, dB, org_stdout, write_callback):
                self.dB = dB
                self.org_stdout = org_stdout
                self.write_callback = write_callback

            def write(self, text):
                self.org_stdout.write(text)
                self.write_callback(text)
                self.dB.update()

            def flush(self):
                self.org_stdout.flush()
        def _append_to_dashboard(text:str):
            if not self.logger.winfo_exists(): return
            self.after(0, lambda: _append_log(text.strip()))
        def _append_log(text):
            self.scroll_offset = 0
            self.log_lines += text.splitlines()
            self.log_lines = self.log_lines[-self.max_lines:]
            self.visible_lines = self.log_lines[-self.max_visible_y:]
            _update_log()
        def _update_log():
            self.logger.itemconfig("logger_txt",text="\n".join(self.visible_lines))
        def _on_mousewheel(event):
            self.scroll_offset += int(event.delta/120)

            total = len(self.log_lines)
            max_scroll = max(0, total - self.max_visible_y)

            self.scroll_offset = max(0, min(self.scroll_offset, max_scroll))

            start = max(0, total - self.max_visible_y - self.scroll_offset)
            end   = total - self.scroll_offset

            self.visible_lines = self.log_lines[start:end]
            _update_log()

        self.logger_font = self.console_font_7
        self.logger_height = 240
        self.logger_width = 120
        padding = 5
        self.logger = tk.Canvas(
            self.side_bar,
            bg=D_GREY,
            width=self.logger_width,
            height=self.logger_height,
            highlightthickness=0
        )
        self.logger.pack(padx=10, pady=10, side='bottom')
        
        self.logger.create_image(0,0,image=self.outline_120x240_TK,anchor='nw',tags=('dash_overlay'))
        self.log_lines = []
        self.visible_lines = []
        self.max_lines = 200
        self.max_visible_y = int((self.logger_height-2*padding)/self.logger_font.metrics("linespace"))
        self.scroll_offset = 0

        self.logger.create_text(
            0, self.logger_height - padding,
            anchor='sw',
            fill=L_GREY,
            font=self.logger_font,
            text="",
            tags=("logger_txt",)
        )
        
        self.logger.bind("<MouseWheel>", _on_mousewheel)
        
        sys.stdout = _DashboardLogger(self, sys.stdout, _append_to_dashboard)
        sys.stderr = _DashboardLogger(self, sys.stderr, _append_to_dashboard)
    
    def clear_canvas(self):
        [self.canvas.delete(id) for id in self.canvas.find_all() if 'keep' not in self.canvas.gettags(id)]
    
    def dash_to_front(self):
        if not self.winfo_exists(): return
        self.canvas.tag_raise("dash_overlay")
        self.dash_after = self.after(100, self.dash_to_front)

    def find_apps(self):
        class app_button():
            def __init__(self, dashboard:Dashboard, name, f_path, app_cls, assets, icon):
                def _launch_app(event):
                    self.launch(self.dB.canvas)
                
                self.dB = dashboard
                self.name = name
                self.f_path = f_path
                self.app_cls = app_cls
                self.assets = assets
                self.icon = icon
                
                self.text_height = 50
                self.photoimg = None
                if self.icon:
                    self.text_height = 90
                    self.photoimg = ImageTk.PhotoImage(Image.open(self.icon).resize((100,100)))
                
                self.btn = tk.Canvas(
                    self.dB.side_bar,
                    bg=D_GREY,
                    width=100,
                    height=100,
                    highlightthickness=0
                )
                self.btn.pack(padx=10, pady=10, side='top')
                self.btn.bind('<Button-1>', _launch_app)
                self.btn.create_image(0,0,image=self.photoimg,anchor='nw')
                if self.photoimg:
                    self.btn.create_image(50,100,image=self.dB.text_bg_100x20_TK,anchor='s')
                self.btn.create_text(50,self.text_height,text=self.name,anchor='center',fill=L_GREY,font=self.dB.font_10_bold)
                self.btn.create_image(0,0,image=self.dB.outline_100x100_TK,anchor='nw')
            def launch(self, ui):
                our_str = '\n'
                our_str += "───────────────────╮\n"
                our_str += "  Initialising app │\n"
                our_str += "╭──────────────────╯\n"
                our_str += f"├─{'─'*len(self.name)}─╮\n"
                our_str += f"╯ {self.name} │\n"
                our_str += f"╭─{'─'*len(self.name)}─╯\n"
                our_str += "╯"
                print(our_str)
                self.dB.clear_canvas()
                self.app = self.app_cls(ui)
                self.app.build_ui()
                
        
        if not APPS_FOLDER.exists():
            APPS_FOLDER.mkdir(parents=True, exist_ok=True)
        self.apps:list[app_button] = []
        for folder in APPS_FOLDER.iterdir():
            if not folder.is_dir():
                continue
            for file in folder.glob("app_*.py"):
                spec = importlib.util.spec_from_file_location(file.stem, file)
                module = importlib.util.module_from_spec(spec)
                spec.loader.exec_module(module)
                app_name = module.__name__
                
                AppClass = getattr(module, "app", None)
                if not AppClass:
                    continue

                assets_path = None
                icon_path = None
                if (folder / 'Assets/').exists():
                    assets_path = folder / 'Assets/'
                    if (folder / 'Assets/icon.png').exists():
                        icon_path = folder / 'Assets/icon.png'
                
                self.apps.append(app_button(self, app_name, file, AppClass, 
                                            assets_path, icon_path))
    
    def dark_mode(self):
        ctypes.windll.dwmapi.DwmSetWindowAttribute(
            ctypes.windll.user32.GetParent(self.winfo_id()),20,
            ctypes.byref(ctypes.c_int(1)),
            ctypes.sizeof(ctypes.c_int(1))
        )

    def set_geometry(self, x_size=None, y_size=None, x_pos=None, y_pos=None):
        out_string = ""
        if (x_size != None) and (y_size != None):
            new_w = min(MAX_WINDOW_SIZE[0], max(x_size, MIN_WINDOW_SIZE[0]))
            new_h = min(MAX_WINDOW_SIZE[1], max(y_size, MIN_WINDOW_SIZE[1]))
            out_string += f"{new_w}x{new_h}"
        if (x_pos != None) and (y_pos != None):
            out_string += f"+{x_pos}+{y_pos}"
        if len(out_string):
            self.geometry(out_string)
        
    def get_images(self):
        arch_10x10 = Image.open(ASSET_FOLDER / "FeatherArch(10x10).png")
        arch_10x10_F = arch_10x10.transpose(Image.FLIP_TOP_BOTTOM)
        
        self.arch_10x10_TK = ImageTk.PhotoImage(arch_10x10)
        self.arch_10x10_F_TK = ImageTk.PhotoImage(arch_10x10_F)
        self.outline_100x100_TK = ImageTk.PhotoImage(Image.open(ASSET_FOLDER / "AppButtonOutline(100x100).png"))
        self.outline_120x240_TK = ImageTk.PhotoImage(Image.open(ASSET_FOLDER / "LoggerOutline(120x240).png"))
        self.text_bg_100x20_TK = ImageTk.PhotoImage(Image.open(ASSET_FOLDER / "AppButtonTextBG(100x20).png"))
        self.outline_16x12_TK = ImageTk.PhotoImage(Image.open(ASSET_FOLDER / "CmdButtonOutline(16x12).png"))
        
if __name__=="__main__":
    if sync_on:
        AppSync.sync_apps_folder(APPS_FOLDER, SERVER_URL)
    root = Dashboard()
    root.mainloop()