"""
Update script for Sky Dragon Editor.

Script version:  1.1
Author:          Jakzie
URL:             https://jakzie.eu/cavestory/editor
License:         https://jakzie.eu/cavestory/editor/license
"""

import os
import re
import shutil
import subprocess
import webbrowser
import ssl
from urllib import request
from zipfile import ZipFile
from typing import Callable

import tkinter as tk
from tkinter import ttk

ENV_VERSION = 1

URL_MAIN = "https://jakzie.eu/cavestory/editor"
URL_CHECK_UPDATE = URL_MAIN + "/releases/latest.txt"
URL_DOWNLOAD = URL_MAIN + "/releases/sde_{}.zip"
NAME_DOWNLOAD = "./sde_{}.zip"
NAME_REQUIREMENTS = "./requirements.txt"

ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE


class SDEUpdate:
    window: tk.Tk
    frame: ttk.Frame | None = None

    update_text: str | None = None
    error: str | None = None

    def __init__(self) -> None:
        self.window = tk.Tk()
        size = 320, 240
        pos_x = (self.window.winfo_screenwidth() / 2) - (size[0] / 2)
        pos_y = (self.window.winfo_screenheight() / 2) - (size[1] / 2)
        self.window.geometry('%dx%d+%d+%d' % (size[0], size[1], pos_x, pos_y))
        self.window.resizable(False, False)
        self.window.title("Sky Dragon Editor - Update")
        self.window.grid_columnconfigure(0, weight=1)
        self.window.grid_rowconfigure(0, weight=1)
        self.window.after_idle(self._state_checking)
        self.window.mainloop()

    def _draw_frame(self) -> None:
        if self.frame is not None:
            self.frame.destroy()
        self.frame = ttk.Frame(self.window)
        self.frame.grid(column=0, row=0, sticky="nsew")
        self.frame.grid_columnconfigure(0, weight=1)
        self.frame.grid_rowconfigure(0, weight=1)

    def _draw_label(self, text: str) -> None:
        ttk.Label(self.frame, text=text, wraplength=300, justify="center").grid(column=0, row=0)

    def _draw_label_link(self, text: str, link: str) -> None:
        l_frame = ttk.Frame(self.frame)
        l_frame.grid(column=0, row=0)
        ttk.Label(l_frame, text=text, wraplength=300, justify="center").grid(column=0, row=0, pady=2)
        link_l = ttk.Label(l_frame, text=link, wraplength=300, justify="center", cursor="hand2", foreground="blue")
        link_l.grid(column=0, row=1, pady=2)
        link_l.bind("<Button-1>", lambda e: webbrowser.open_new_tab(link))

    def _draw_button(self, text: str, command: Callable[[], None]) -> None:
        ttk.Button(self.frame, text=text, padding=7, width=12, command=command).grid(column=0, row=1, pady=15)

    def _draw_buttons(self, text1: str, command1: Callable[[], None], text2: str, command2: Callable[[], None]) -> None:
        b_frame = ttk.Frame(self.frame)
        b_frame.grid(column=0, row=1, pady=15)
        ttk.Button(b_frame, text=text1, padding=7, width=12, command=command1).grid(column=0, row=0, padx=5)
        ttk.Button(b_frame, text=text2, padding=7, width=12, command=command2).grid(column=1, row=0, padx=5)

    def _state_checking(self) -> None:
        self._draw_frame()
        self._draw_label("Checking for updates...")
        self._draw_button("Cancel", self.window.destroy)
        self.window.update_idletasks()

        cur_ver = "0"
        try:
            with open("./SkyDragonEditor.bat", "r", encoding="utf-8") as file:
                for line in file:
                    if line.startswith(":: Version: "):
                        cur_ver = line[11:].strip()
                        break
        except OSError:
            pass

        try:
            with request.urlopen(URL_CHECK_UPDATE, context=ctx) as response:
                self.update_text, min_env_ver = re.sub(r"[/\\@%]", "", response.read().decode("utf-8").strip()).split("\n")
            min_env_ver_i = int(min_env_ver)
        except (OSError, ValueError) as e:
            self.error = "Failed to check for updates: " + str(e)
            self.window.after_idle(self._state_error)
        else:
            if ENV_VERSION < min_env_ver_i:
                self.window.after_idle(self._state_unsupported)
            elif self.update_text == cur_ver:
                self.window.after_idle(self._state_newest)
            else:
                self.window.after_idle(self._state_found)

    def _state_error(self) -> None:
        self._draw_frame()
        self._draw_label(self.error)
        self._draw_button("Close", self.window.destroy)

    def _state_unsupported(self) -> None:
        self._draw_frame()
        self._draw_label_link("This SDE environment is no longer supported. Download a new one from:", URL_MAIN)
        self._draw_button("Close", self.window.destroy)

    def _state_newest(self) -> None:
        self._draw_frame()
        self._draw_label("No newer version available.")
        self._draw_buttons("Redownload", self._state_download, "Close", self.window.destroy)

    def _state_found(self) -> None:
        self._draw_frame()
        self._draw_label("New version found: " + self.update_text)
        self._draw_buttons("Download", self._state_download, "Close", self.window.destroy)

    def _state_download(self) -> None:
        self._draw_frame()
        self._draw_label("Downloading version " + self.update_text + "...")
        self._draw_button("Cancel", self.window.destroy)
        self.window.update_idletasks()

        try:
            with request.urlopen(URL_DOWNLOAD.format(self.update_text), context=ctx) as response:
                with open(NAME_DOWNLOAD.format(self.update_text), "wb") as file:
                    shutil.copyfileobj(response, file)
        except (OSError, ValueError) as e:
            self.error = "Download failed: " + str(e)
            self.window.after_idle(self._state_error)
        else:
            self.window.after_idle(self._state_install)

    def _state_install(self) -> None:
        self._draw_frame()
        self._draw_label("Unpacking version " + self.update_text + "...")
        self._draw_button("Cancel", self.window.destroy)
        self.window.update_idletasks()

        try:
            with ZipFile(NAME_DOWNLOAD.format(self.update_text), "r") as zip_file:
                zip_file.extractall(path="./")
        except (OSError, ValueError) as e:
            self.error = "Unpacking failed: " + str(e)
            self.window.after_idle(self._state_error)
        else:
            try:
                os.remove(NAME_DOWNLOAD.format(self.update_text))
            except (OSError, ValueError):
                pass
            self.window.after_idle(self._state_dependency)

    def _state_dependency(self) -> None:
        self._draw_frame()
        self._draw_label("Downloading dependencies...")
        self._draw_button("Cancel", self.window.destroy)
        self.window.update_idletasks()

        try:
            if not os.path.isfile("./lib/python/Scripts/pip.exe"):
                subprocess.run(["./lib/python/python.exe", "./lib/get-pip/get-pip.py"], check=True, creationflags=subprocess.CREATE_NO_WINDOW)
            subprocess.run(["./lib/python/python.exe", "-m", "pip", "install", "-r", NAME_REQUIREMENTS], check=True, creationflags=subprocess.CREATE_NO_WINDOW)
        except (OSError, ValueError, subprocess.SubprocessError) as e:
            self.error = "Failed to setup dependencies: " + str(e)
            self.window.after_idle(self._state_error)
        else:
            self.window.after_idle(self._state_done)

    def _state_done(self) -> None:
        self._draw_frame()
        self._draw_label("Version " + self.update_text + " downloaded successfully.")
        self._draw_button("Close", self.window.destroy)


if __name__ == "__main__":
    SDEUpdate()
