#!/usr/bin/env python3

#This work's license :  MIT License
#Copyright CIDE li Studio 2026 ©

import json
import os
import platform
import subprocess
import sys
import time
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

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"]))
    Lang = config_data["Lang"]
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"


class LangApply:
        lang_file = get_config_path(f"data/lang/{Lang}.json")
        with open(lang_file, "r") as f:
           lang_data = json.load(f)
        model_preview = lang_data["model_preview"]
        model_insert = lang_data["model_insert"]
        input_text_tip = lang_data["input_text_tip"]
        showerror_title = lang_data["showerror_title"]
        showerror_message = lang_data["showerror_message"]
        showerror_read_error = lang_data["showerror_read_error"]
        open_file_title = lang_data["open_file_title"]
        open_file_tip = lang_data["open_file_tip"]
        showinfo_no_insert_title = lang_data["showinfo_no_insert_title"]
        showinfo_no_insert_message = lang_data["showinfo_no_insert_message"]
        showerror_PermissionError = lang_data["showerror_PermissionError"]
        showerror_FileNotFoundError = lang_data["showerror_FileNotFoundError"]
        showerror_UnicodeDecodeError = lang_data["showerror_UnicodeDecodeError"]
        showerror_InternalError = lang_data["showerror_InternalError"]
        showerror_NotCode = lang_data["showerror_NotCode"]
        showerror_TimeOut = lang_data["showerror_TimeOut"]
        tip_title_clear = lang_data["tip_title_clear"]
        tip_message_clear = lang_data["tip_message_clear"]
        Run_cide_Error = lang_data["Run_cide_Error"]

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"



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

def lang(config_ui, config_text):
    config_ui.config(text=config_text)


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("https://cide.eu.cc"))
    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=LangApply.model_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", LangApply.input_text_tip)
# noinspection bad-argument-type
input_text.config(state="disabled")


# noinspection bad-argument-type
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=LangApply.model_insert)
        if insert_text == LangApply.input_text_tip:
            input_text.delete("1.0", "end")
        else:
            return
    else:
        return


# noinspection bad-argument-type
def off_insert(event=None):
    global insert
    if insert:
        insert = False
        input_text.config(state="disabled")
        model.config(text=LangApply.model_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 == LangApply.input_text_tip:
        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=LangApply.showerror_title, message=f"{LangApply.showerror_message}\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=LangApply.showerror_title, message=LangApply.showerror_read_error)
            return
    path = simpledialog.askstring(title=LangApply.open_file_title, prompt=LangApply.open_file_tip)
    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=LangApply.showinfo_no_insert_title, message=LangApply.showinfo_no_insert_message)
            return
        return
    except PermissionError:
        messagebox.showerror(title=LangApply.showerror_title, message=LangApply.showerror_PermissionError)
        return
    except FileNotFoundError:
        messagebox.showerror(title=LangApply.showerror_title, message=LangApply.showerror_FileNotFoundError)
        return
    except UnicodeDecodeError:
        messagebox.showerror(title=LangApply.showerror_title, message=LangApply.showerror_UnicodeDecodeError)
        return
    except:
        messagebox.showerror(title=LangApply.showerror_title, message=LangApply.showerror_InternalError)
        return


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

    if not content or content == LangApply.input_text_tip:
        messagebox.showerror(title=LangApply.showerror_title, message=LangApply.showerror_NotCode)
        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=LangApply.showerror_title, message=f"{LangApply.showerror_message}\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=LangApply.showerror_title, message=f"{LangApply.showerror_message}\n{run_output.stderr}")
                run_code_tmp()
                return

        messagebox.showinfo(title="CIDE-run", message=run_output.stdout)
    except subprocess.TimeoutExpired:
        messagebox.showerror(title=LangApply.showerror_title,
                             message=f"{LangApply.showerror_message}\n{LangApply.showerror_TimeOut}")
    run_code_tmp()


def clear(event=None):
    user_select = messagebox.askokcancel(title=LangApply.tip_title_clear, message=LangApply.tip_message_clear)
    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=LangApply.Run_cide_Error)
else:
    root.mainloop()
