#!/usr/bin/env python3

import json
import os
import platform
import subprocess
import sys
import tkinter as tk
import traceback
import webbrowser
from pathlib import Path
from tkinter import messagebox
from tkinter import scrolledtext
from tkinter import simpledialog

from pygments.lexers import CLexer
from pygments.token import Token


class Color:
    RED = "\033[31m"
    GREEN = "\033[32m"
    BLUE = "\033[34m"
    YELLOW = "\033[33m"
    MAGENTA = "\033[35m"
    CYAN = "\033[36m"
    WHITE = "\033[37m"
    END = "\033[0m"


def get_base_path():
    if getattr(sys, 'frozen', False):
        return Path(sys._MEIPASS)
    return Path(__file__).resolve().parent


def get_config_path(path):
    if path is not None and path != "":
        base_path = get_base_path()
        return base_path / path
    base = get_base_path()
    paths = [
        base / "data" / "config.json",
        Path("/etc/cide/config.json"),
        base.parent / "data" / "config.json",
    ]
    for p in paths:
        if p.exists():
            return p
    return paths[0]


config_path = get_config_path(None)
try:
    with open(config_path, "r") as f:
        config_data = json.load(f)
    run_time_out = config_data["run_time_out"]
    topic = config_data["theme"]
    tab_c_num = eval(str(config_data["Tab_C_NUM"]))
except:
    messagebox.showinfo(title="JSON file", message="Can't find the JSON file, restored to default values")
    run_time_out = 3
    topic = "Highlight"
    tab_c_num = 4

theme_config_path = get_config_path(f"data/theme/{topic}.json")

try:
    with open(theme_config_path, "r") as f:
        content = f.read()
        theme_config_data = json.loads(content)[0]
        topic_author = theme_config_data["author"]
        if topic_author == "":
            topic_author = "No publisher"
        elif topic_author == "CIDE":
            topic_author = "CIDE ✅"

        topic_introduce = theme_config_data["introduce"]
        if topic_introduce == "None" or topic_introduce == "":
            topic_introduce = "No introduction"

    token_styles = {
        Token.Keyword: theme_config_data["Token.Keyword"],
        Token.Keyword.Constant: theme_config_data["Token.Keyword.Constant"],
        Token.Keyword.Declaration: theme_config_data["Token.Keyword.Declaration"],
        Token.Keyword.Namespace: theme_config_data["Token.Keyword.Namespace"],
        Token.Keyword.Pseudo: theme_config_data["Token.Keyword.Pseudo"],
        Token.Keyword.Reserved: theme_config_data["Token.Keyword.Reserved"],
        Token.Keyword.Type: theme_config_data["Token.Keyword.Type"],
        Token.String: theme_config_data["Token.String"],
        Token.String.Doc: theme_config_data["Token.String.Doc"],
        Token.Comment: theme_config_data["Token.Comment"],
        Token.Comment.Single: theme_config_data["Token.Comment.Single"],
        Token.Comment.Multiline: theme_config_data["Token.Comment.Multiline"],
        Token.Comment.Preproc: theme_config_data["Token.Comment.Preproc"],
        Token.Number: theme_config_data["Token.Number"],
        Token.Number.Float: theme_config_data["Token.Number.Float"],
        Token.Number.Hex: theme_config_data["Token.Number.Hex"],
        Token.Number.Integer: theme_config_data["Token.Number.Integer"],
        Token.Number.Oct: theme_config_data["Token.Number.Oct"],
        Token.Name.Function: theme_config_data["Token.Name.Function"],
        Token.Name.Class: theme_config_data["Token.Name.Class"],
        Token.Name.Builtin: theme_config_data["Token.Name.Builtin"],
        Token.Operator: theme_config_data["Token.Operator"],
        Token.Punctuation: theme_config_data["Token.Punctuation"],
        Token.Generic.Deleted: theme_config_data["Token.Generic.Deleted"],
        Token.Generic.Inserted: theme_config_data["Token.Generic.Inserted"],
        Token.Text: theme_config_data["Token.Text"],
    }
except:
    messagebox.showerror(title="JSON file", message=f"Can't find theme file: {topic}.json, restored to default values")
    token_styles = {}
    theme_config_data = {}
    topic_author = "Unknown"
    topic_introduce = "Unknown"

font = "Arial, Microsoft YaHei, Noto Sans"
font_size = 16
os_system = platform.system()
os_user = os.getlogin()
tmp_file = "/tmp/c-ide.c"
full_edit = False

if os_system == "Darwin":
    os_system = "MacOS"
elif os_system == "Windows":
    tmp_file = f"C:/Users/{os_user}/AppData/Local/tmp/c-ide.tmp"

insert = False
tmp_v = ""


def save_file():
    input_path = simpledialog.askstring(title="Save File?", prompt="Path:")
    if input_path is None or input_path == "":
        return
    with open(f"{input_path}", "w", encoding='utf-8') as f:
        f.write(f'{user_input.get()}')


def order(event=None):
    order_input = simpledialog.askstring(title="run-command", prompt="Run this command: ")
    if order_input is None or order_input == "":
        return
    elif order_input == ":wq":
        save_file()
    elif order_input == ":q":
        confirm_exit = messagebox.askokcancel(title="Confirm Exit?", message="Do you want to exit?")
        if confirm_exit:
            root.destroy()
            exit()
        else:
            return
    elif order_input == ":run":
        run_code()
    elif order_input == ":o":
        open_file("")
        return
    elif order_input == ":w":
        save_file()
    elif order_input == r"\pyshell":
        pyshell()
    elif order_input == ":s":
        settings()
    elif order_input == ":theme-info":
        messagebox.showinfo(title=f"Theme {topic} Info",
                            message=f"Author: {topic_author}\nIntroduce: {topic_introduce}")
    else:
        try:
            output = subprocess.run(f'{order_input}', shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
                                    text=True, check=True)
            messagebox.showinfo(title="from Shell", message=output.stdout)
            return
        except subprocess.CalledProcessError as e:
            messagebox.showerror(title="from Shell", message=f"{e.stderr}")
            return


def open_url(url):
    webbrowser.open_new(url)


def apply_theme(widget, config_key):
    if not theme_config_data:
        return

    bg_key = f"{config_key}.bg"
    fg_key = f"{config_key}.fg"
    insert_key = f"{config_key}.insertbackground"

    if bg_key in theme_config_data:
        try:
            widget.config(bg=theme_config_data[bg_key])
        except:
            pass

    if fg_key in theme_config_data:
        try:
            widget.config(fg=theme_config_data[fg_key])
        except:
            pass

    if insert_key in theme_config_data:
        try:
            widget.config(insertbackground=theme_config_data[insert_key])
        except:
            pass


root = tk.Tk()
root.title(f"CIDE | {os_system}")
root.attributes("-fullscreen", False)
root.geometry("1400x1100")
apply_theme(root, "root")


def pyshell():
    global font
    pyshell_root = tk.Tk()
    pyshell_root.attributes("-fullscreen", True)
    pyshell_root.title("pyshell")
    apply_theme(pyshell_root, "pyshell")

    input_shell_text = tk.StringVar()
    input_shell_text.set("")

    input_shell = scrolledtext.ScrolledText(pyshell_root, wrap=tk.CHAR, width=150, font=(font, 18), height=70)
    input_shell.pack(pady=0, padx=0)
    apply_theme(input_shell, "pyshell")

    input_shell.config(state="normal")

    def update_shell_input(event=None):
        input_update = input_shell.get("1.0", "end-1c")
        input_shell_text.set(input_update)

    input_shell.bind("<Key>", update_shell_input)

    def shell_x(event=None):
        try:
            return_code = eval(input_shell_text.get())
            input_shell.delete("1.0", "end")
            input_shell.insert("1.0", f"{return_code}")
        except:
            input_shell.insert("1.0", "\nERROR")

    input_shell.bind("<F5>", shell_x)

    pyshell_root.mainloop()


def settings(event=None):
    settings_menu = tk.Toplevel(root)
    settings_menu.title("Settings")
    settings_menu.geometry("800x600")
    settings_menu.transient(root)
    apply_theme(settings_menu, "settings_menu")

    top_title = tk.Label(settings_menu, text="Settings - Test", font=(font, 20))
    top_title.pack(pady=(10, 20))
    apply_theme(top_title, "top_title")

    open_url_official_website = tk.Button(settings_menu, text="Visit the official website", font=(font, 12),
                                          command=lambda: open_url("http://43.159.61.46"))
    open_url_official_website.pack(pady=(10, 5))
    apply_theme(open_url_official_website, "open_url_official_website")

    tip_time_out_settings = tk.Label(settings_menu, text="Timeout settings", font=(font, 16))
    tip_time_out_settings.pack(pady=(10, 2))
    apply_theme(tip_time_out_settings, "tip_time_out_settings")

    time_out_settings = tk.Entry(settings_menu, font=(font, 18), width=20)
    time_out_settings.pack(pady=(10, 15))
    apply_theme(time_out_settings, "time_out_settings")

    settings_menu.mainloop()


user_input = tk.StringVar()
user_input.set("")

title_label = tk.Label(root, text=f"CIDE 1.0812 | {os_system}", font=(font, 18))
title_label.pack(pady=(10, 20))
apply_theme(title_label, "root.title_label")

model = tk.Label(root, text="--PREVIEW--", font=(font, 12))
model.pack(pady=(5, 10))
apply_theme(model, "root.model")

word_num = tk.Label(root, text=f"0", font=(font, 12), width=10)
word_num.pack(pady=(10, 10))
apply_theme(word_num, "root.word_num")

input_text = tk.scrolledtext.ScrolledText(root, wrap=tk.CHAR, width=150, font=(font, font_size), height=70)
input_text.pack(pady=(10, 10), padx=(10, 10))
apply_theme(input_text, "root.input_text")

input_text.insert("1.0", "Click I to edit.")
input_text.config(state="disabled")


def on_insert(event=None):
    global insert
    insert_text = input_text.get("1.0", "end-1c")
    if not insert:
        insert = True
        input_text.config(state="normal")
        model.config(text="--INSERT--")
        if insert_text == "Click I to edit.":
            input_text.delete("1.0", "end")
        else:
            return
    else:
        return


def off_insert(event=None):
    global insert
    if insert:
        insert = False
        input_text.config(state="disabled")
        model.config(text="--PREVIEW--")
    else:
        return


highlight_after_id = None


def apply_syntax_highlighting():
    global highlight_after_id, token_styles
    highlight_after_id = None
    content = input_text.get("1.0", "end-1c")
    if not content or content == "Click I to edit.":
        return
    cursor_pos = input_text.index(tk.INSERT)
    for tag in input_text.tag_names():
        if tag.startswith("hl_"):
            input_text.tag_delete(tag)

    lexer = CLexer()
    tokens = list(lexer.get_tokens(content))
    pos = 0
    for token_type, token_text in tokens:
        if token_text.strip():
            start_idx = f"1.0 + {pos} chars"
            end_idx = f"1.0 + {pos + len(token_text)} chars"
            color = None
            for token_pattern, style_color in token_styles.items():
                if token_type in token_pattern:
                    color = style_color
                    break
            if color:
                tag_name = f"hl_{token_type}_{pos}"
                input_text.tag_add(tag_name, start_idx, end_idx)
                try:
                    input_text.tag_config(tag_name, foreground=color)
                except:
                    pass
        pos += len(token_text)
    input_text.mark_set(tk.INSERT, cursor_pos)


def syntax_highlight_delayed(event=None):
    global highlight_after_id
    if highlight_after_id is not None:
        input_text.after_cancel(highlight_after_id)
    highlight_after_id = input_text.after(500, apply_syntax_highlighting)


def update_input(event=None):
    content = input_text.get("1.0", "end-1c")
    user_input.set(content)
    word_num.config(text=f"{len(input_text.get('1.0', 'end-1c'))}")
    syntax_highlight_delayed()


def font_size_enlarge(event=None):
    global font_size
    if font_size <= 120:
        font_size = font_size + 1
        input_text.config(font=(font, font_size))
    else:
        return


def font_size_de(event=None):
    global font_size
    if font_size >= 12:
        font_size = font_size - 1
        input_text.config(font=(font, font_size))
    else:
        return


def run_code_tmp():
    global tmp_v
    tmp_v = tmp_file.removesuffix(".c")
    try:
        if os.path.exists(f"{tmp_file}"):
            os.remove(f"{tmp_file}")
        if os.path.exists(f"{tmp_v}"):
            os.remove(f"{tmp_v}")
    except:
        messagebox.showerror(title="Error", message=f"Error info:\n{traceback.format_exc()}")
        return


def open_file(open_file_name, event=None):
    if open_file_name == "edit":
        base = get_base_path()
        config_file_path = base / "data" / "config.json"
        try:
            with open(config_file_path, "r", encoding='utf-8') as f:
                content = f.read()
            user_input.set(content)
            input_text.insert("1.0", content)
            off_insert()
            return
        except:
            messagebox.showerror(title="Error", message=f"read error")
            return
    path = simpledialog.askstring(title="Open File", prompt="Please enter the file path")
    try:
        if insert:
            with open(f"{path}", "r", encoding='utf-8') as f:
                content = f.read()
            user_input.set(content)
            input_text.insert("1.0", content)
            off_insert()
        else:
            messagebox.showinfo(title="No insert", message="Please turn on insert first")
            return
        return
    except PermissionError:
        messagebox.showerror(title="Error", message="Permission denied")
        return
    except FileNotFoundError:
        messagebox.showerror(title="Error", message="File not found")
        return
    except UnicodeDecodeError:
        messagebox.showerror(title="Error", message="Incorrect encoding")
        return
    except:
        messagebox.showerror(title="Error", message="Internal error")
        return


def run_code(event=None):
    global tmp_v
    content = input_text.get("1.0", "end-1c").strip()

    if not content or content == "Click I to edit.":
        messagebox.showerror(title="Error", message="No code to compile!")
        return

    with open(f"{tmp_file}", "w", encoding='utf-8') as f:
        f.write(content)
    tmp_v = tmp_file.removesuffix(".c")
    output = subprocess.run(["gcc", f"{tmp_file}", "-o", f"{tmp_v}"], shell=False, stdout=subprocess.PIPE,
                            stderr=subprocess.PIPE, text=True)

    if output.returncode != 0:
        messagebox.showerror(title="Error", message=f"Error info:\n{output.stderr}")
        run_code_tmp()
        return

    try:
        run_output = subprocess.run([f"{tmp_v}"], shell=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
                                    text=True, timeout=run_time_out)
        if not run_output.stdout.strip():
            messagebox.showwarning(title="Warning", message=f"info:\nThe returned content is empty, you should fix it.")
            return
        else:
            if run_output.returncode != 0:
                messagebox.showerror(title="Error", message=f"Error info:\n{run_output.stderr}")
                run_code_tmp()
                return

        messagebox.showinfo(title="CIDE-run", message=run_output.stdout)
    except subprocess.TimeoutExpired:
        messagebox.showerror(title="Error",
                             message=f"Error info:\nExecution timed out and was forcibly terminated.\n\nPlease check for a fork bomb or an infinite loop.")
    run_code_tmp()


def clear(event=None):
    user_select = messagebox.askokcancel(title="Empty content?", message="Are you sure you want to clear the content?")
    if user_select:
        input_text.delete("1.0", "end")
        user_input.set("")
    else:
        return


def full_edited(event=None):
    global full_edit
    if full_edit:
        full_edit = False
        root.attributes("-fullscreen", False)
    else:
        full_edit = True
        root.attributes("-fullscreen", True)


def tab_cpt(event=None):
    input_text.insert(tk.INSERT, " " * tab_c_num)
    return "break"


input_text.bind("<KeyRelease>", update_input)
root.bind("<i>", on_insert)
root.bind("<Control-Button-4>", font_size_enlarge)
root.bind("<Control-Button-5>", font_size_de)
root.bind("<Escape>", off_insert)
root.bind("<Alt-F5>", order)
root.bind("<Control-e>", clear)
root.bind("<F5>", run_code)
root.bind("<F11>", full_edited)
input_text.bind("<Tab>", tab_cpt)

if __name__ != "__main__":
    root.destroy()
    messagebox.showwarning(title="CIDE", message="Please run it directly CIDE")
else:
    root.mainloop()
