import tkinter as tk
from tkinter import messagebox
from tkinter import ttk
import tkinter.font as tkfont
import json
from tkinter import filedialog

class CreditsApp:
    def __init__(self, root):
        self.root = root
        self.root.title("Credits Script Creator")

        # Canvas dimensions
        self.canvas_width = 256
        self.canvas_height = 192

        # Initialize pages
        self.pages = {}
        self.current_page = 0

        # Currently selected text item
        self.selected_item = None
        self.dragging = False

        # Guide lines settings
        self.show_guides = tk.BooleanVar(value=False)
        self.guide_color = tk.StringVar(value='black')
        self.vertical_lines = tk.IntVar(value=0)
        self.horizontal_lines = tk.IntVar(value=0)

        # Define fonts
        self.normal_font = tkfont.Font(family='TkDefaultFont', size=10)
        self.bold_font = tkfont.Font(family='TkDefaultFont', size=10, weight='bold')

        # Create UI elements
        self.create_widgets()

        # Bind arrow keys
        self.root.bind('<Left>', self.prev_page)
        self.root.bind('<Right>', self.next_page)

    def create_widgets(self):
        # Frame for canvas and controls
        self.frame = ttk.Frame(self.root)
        self.frame.pack(padx=10, pady=10)

        # Canvas
        self.canvas = tk.Canvas(self.frame, width=self.canvas_width, height=self.canvas_height, bg='white')
        self.canvas.pack()
        self.canvas.bind("<Button-1>", self.on_canvas_click)
        self.canvas.bind("<ButtonPress-1>", self.on_canvas_press)
        self.canvas.bind("<B1-Motion>", self.on_canvas_drag)
        self.canvas.bind("<ButtonRelease-1>", self.on_canvas_release)

        # Page label
        self.page_label = ttk.Label(self.frame, text=f"Page: {self.current_page}")
        self.page_label.pack()

        # Page navigation buttons
        nav_frame = ttk.Frame(self.frame)
        nav_frame.pack(pady=5)
        self.prev_button = ttk.Button(nav_frame, text="Previous Page", command=self.prev_page)
        self.prev_button.pack(side=tk.LEFT)
        self.next_button = ttk.Button(nav_frame, text="Next Page", command=self.next_page)
        self.next_button.pack(side=tk.LEFT)

        # Add Text button
        self.add_text_button = ttk.Button(self.frame, text="Add Text", command=self.add_text_center)
        self.add_text_button.pack(side=tk.LEFT, pady=5)

        # Save and Load buttons
        self.save_button = ttk.Button(self.frame, text="Save", command=self.save_data)
        self.save_button.pack(side=tk.LEFT, padx=5)

        self.load_button = ttk.Button(self.frame, text="Load", command=self.load_data)
        self.load_button.pack(side=tk.LEFT, padx=5)

        # Generate button
        self.generate_button = ttk.Button(self.frame, text="Generate CPP Code", command=self.generate_cpp_code)
        self.generate_button.pack(side=tk.LEFT, pady=5)

        # Warning label for character limit
        self.warning_label = ttk.Label(self.frame, text="", foreground="red")
        self.warning_label.pack()

        # Guide lines controls
        guide_frame = ttk.Frame(self.root)
        guide_frame.pack(padx=10, pady=5, fill=tk.X)

        # Show guides checkbox
        self.show_guides_checkbox = ttk.Checkbutton(guide_frame, text="Show Guide Lines", variable=self.show_guides, command=self.refresh_canvas)
        self.show_guides_checkbox.grid(row=0, column=0, sticky=tk.W)

        # Guide color combobox
        ttk.Label(guide_frame, text="Guide Line Color:").grid(row=0, column=1, sticky=tk.W)
        self.guide_color_combo = ttk.Combobox(guide_frame, textvariable=self.guide_color, values=['black', 'light grey', 'pink'], state='readonly')
        self.guide_color_combo.grid(row=0, column=2, sticky=tk.W)
        self.guide_color_combo.bind("<<ComboboxSelected>>", lambda e: self.refresh_canvas())

        # Vertical lines spinbox
        ttk.Label(guide_frame, text="Vertical Lines:").grid(row=1, column=0, sticky=tk.W)
        self.vertical_lines_spin = ttk.Spinbox(guide_frame, from_=0, to=10, textvariable=self.vertical_lines, width=5, command=self.refresh_canvas)
        self.vertical_lines_spin.grid(row=1, column=1, sticky=tk.W)
        self.vertical_lines_spin.bind("<KeyRelease>", lambda e: self.refresh_canvas())

        # Horizontal lines spinbox
        ttk.Label(guide_frame, text="Horizontal Lines:").grid(row=1, column=2, sticky=tk.W)
        self.horizontal_lines_spin = ttk.Spinbox(guide_frame, from_=0, to=10, textvariable=self.horizontal_lines, width=5, command=self.refresh_canvas)
        self.horizontal_lines_spin.grid(row=1, column=3, sticky=tk.W)
        self.horizontal_lines_spin.bind("<KeyRelease>", lambda e: self.refresh_canvas())

        # Frame for selected item controls
        self.selected_frame = ttk.Frame(self.root)
        self.selected_frame.pack(padx=10, pady=10, fill=tk.X)

        # Variables for selected item properties
        self.text_var = tk.StringVar()
        self.palette_var = tk.StringVar()
        self.start_page_var = tk.IntVar()
        self.end_page_var = tk.IntVar()
        self.y_var = tk.IntVar()

        # Controls for selected item
        self.create_selected_item_controls()

        # Initially disable the controls
        self.disable_selected_item_controls()

    def create_selected_item_controls(self):
        # Text label and entry
        ttk.Label(self.selected_frame, text="Text:").grid(row=0, column=0, sticky=tk.W)
        self.text_entry = ttk.Entry(self.selected_frame, textvariable=self.text_var)
        self.text_entry.grid(row=0, column=1, sticky=tk.W+tk.E, columnspan=3)
        self.text_entry.bind("<KeyRelease>", self.on_text_change)

        # Palette label and combobox
        ttk.Label(self.selected_frame, text="Palette:").grid(row=1, column=0, sticky=tk.W)
        self.palette_combo = ttk.Combobox(self.selected_frame, textvariable=self.palette_var, values=['Red', 'Blue'], state='readonly')
        self.palette_combo.grid(row=1, column=1, sticky=tk.W)
        self.palette_combo.bind("<<ComboboxSelected>>", self.on_palette_change)

        # Y position label and spinbox
        ttk.Label(self.selected_frame, text="Y:").grid(row=1, column=2, sticky=tk.W)
        self.y_spin = ttk.Spinbox(self.selected_frame, from_=0, to=self.canvas_height, textvariable=self.y_var, width=5, command=self.on_position_change)
        self.y_spin.grid(row=1, column=3, sticky=tk.W)
        self.y_spin.bind("<KeyRelease>", lambda e: self.on_position_change())

        # Start page label and spinbox
        ttk.Label(self.selected_frame, text="Start Page:").grid(row=2, column=0, sticky=tk.W)
        self.start_page_spin = ttk.Spinbox(self.selected_frame, from_=0, to=999, textvariable=self.start_page_var, width=5, command=self.on_multipage_change)
        self.start_page_spin.grid(row=2, column=1, sticky=tk.W)
        self.start_page_spin.bind("<KeyRelease>", lambda e: self.on_multipage_change())

        # End page label and spinbox
        ttk.Label(self.selected_frame, text="End Page:").grid(row=2, column=2, sticky=tk.W)
        self.end_page_spin = ttk.Spinbox(self.selected_frame, from_=0, to=999, textvariable=self.end_page_var, width=5, command=self.on_multipage_change)
        self.end_page_spin.grid(row=2, column=3, sticky=tk.W)
        self.end_page_spin.bind("<KeyRelease>", lambda e: self.on_multipage_change())

        # Delete button
        self.delete_button = ttk.Button(self.selected_frame, text="Delete", command=self.delete_selected_item)
        self.delete_button.grid(row=3, column=0, columnspan=4, pady=5)

        # Configure column weights
        self.selected_frame.columnconfigure(1, weight=1)
        self.selected_frame.columnconfigure(3, weight=1)

    def enable_selected_item_controls(self):
        self.text_entry.config(state='normal')
        self.palette_combo.config(state='readonly')
        self.start_page_spin.config(state='normal')
        self.end_page_spin.config(state='normal')
        self.delete_button.config(state='normal')
        self.y_spin.config(state='normal')

    def disable_selected_item_controls(self):
        self.text_entry.config(state='disabled')
        self.palette_combo.config(state='disabled')
        self.start_page_spin.config(state='disabled')
        self.end_page_spin.config(state='disabled')
        self.delete_button.config(state='disabled')
        self.y_spin.config(state='disabled')

    def on_canvas_click(self, event):
        # Left-click selects or deselects items
        x, y = event.x, event.y
        tolerance = 2  # Small tolerance for detecting nearby items
        items = self.canvas.find_overlapping(x - tolerance, y - tolerance, x + tolerance, y + tolerance)
        item_clicked = None
        for item in items:
            tags = self.canvas.gettags(item)
            if 'text_item' in tags:
                item_clicked = item
                break

        if item_clicked:
            # Select the text item
            self.selected_item = item_clicked
            self.highlight_selected_item()
            # Bring the selected item to the front
            self.canvas.tag_raise(self.selected_item)
            # Update selected item controls
            self.update_selected_item_controls()
        else:
            # Deselect any previously selected item
            self.selected_item = None
            self.highlight_selected_item()
            self.disable_selected_item_controls()

    def update_selected_item_controls(self):
        # Enable controls
        self.enable_selected_item_controls()
        # Get the entry corresponding to the selected item
        entry = self.get_entry_by_canvas_item(self.selected_item)
        if not entry:
            return
        # Update variables
        self.text_var.set(entry['text'])
        self.palette_var.set(entry['palette'])
        self.start_page_var.set(entry['start_page'])
        self.end_page_var.set(entry['end_page'])
        self.y_var.set(int(entry['y']))

    def on_text_change(self, event):
        if self.selected_item:
            entry = self.get_entry_by_canvas_item(self.selected_item)
            if not entry:
                return
            new_text = self.text_var.get()
            # Disallow numbers
            if any(char.isdigit() for char in new_text):
                messagebox.showerror("Error", "Text cannot contain numbers.")
                self.text_var.set(entry['text'])
                return
            # Enforce rules
            if len(new_text) > 41:
                messagebox.showerror("Error", "Text exceeds 41 characters limit.")
                self.text_var.set(entry['text'])
                return
            if new_text.startswith(' ') or new_text.startswith('\0'):
                messagebox.showerror("Error", "Text cannot start with a space or null terminator.")
                self.text_var.set(entry['text'])
                return
            # Update the text in the data structure
            entry['text'] = new_text
            # Update the text on the canvas
            self.canvas.itemconfig(self.selected_item, text=new_text)
            # Update character warning
            self.update_character_warning()

    def on_palette_change(self, event):
        if self.selected_item:
            entry = self.get_entry_by_canvas_item(self.selected_item)
            if not entry:
                return
            new_palette = self.palette_var.get()
            entry['palette'] = new_palette
            # Update the text color on the canvas
            self.canvas.itemconfig(self.selected_item, fill=new_palette.lower())

    def on_multipage_change(self):
        if self.selected_item:
            entry = self.get_entry_by_canvas_item(self.selected_item)
            if not entry:
                return
            try:
                start_page = int(self.start_page_var.get())
                end_page = int(self.end_page_var.get())
                if start_page > end_page:
                    messagebox.showerror("Error", "Start page cannot be greater than end page.")
                    self.start_page_var.set(entry['start_page'])
                    self.end_page_var.set(entry['end_page'])
                    return
                entry['start_page'] = start_page
                entry['end_page'] = end_page
                entry['multipage'] = True if start_page != end_page else False
                # Update the pages where this entry should appear
                self.update_entry_pages(entry)
                # Refresh canvas to show/hide the entry on the current page
                self.refresh_canvas()
            except ValueError:
                messagebox.showerror("Error", "Invalid page number.")

    def on_position_change(self):
        if self.selected_item:
            entry = self.get_entry_by_canvas_item(self.selected_item)
            if not entry:
                return
            try:
                new_y = int(self.y_var.get())
                entry['y'] = new_y
                # Update the item's position on the canvas
                self.canvas.coords(self.selected_item, self.canvas_width / 2, new_y)
            except ValueError:
                messagebox.showerror("Error", "Invalid Y coordinate.")

    def on_canvas_press(self, event):
        # Start dragging if a text item is under the cursor
        x, y = event.x, event.y
        tolerance = 2
        items = self.canvas.find_overlapping(x - tolerance, y - tolerance, x + tolerance, y + tolerance)
        for item in items:
            tags = self.canvas.gettags(item)
            if 'text_item' in tags:
                self.selected_item = item
                self.highlight_selected_item()
                self.dragging = True
                self.drag_start_y = event.y
                # Bring the selected item to the front
                self.canvas.tag_raise(self.selected_item)
                # Update selected item controls
                self.update_selected_item_controls()
                break

    def on_canvas_drag(self, event):
        if self.dragging and self.selected_item:
            dy = event.y - self.drag_start_y
            self.canvas.move(self.selected_item, 0, dy)
            self.drag_start_y = event.y
            # Update the position in the data structure
            self.update_entry_position(self.selected_item, dy)
            # Update the Y variable
            entry = self.get_entry_by_canvas_item(self.selected_item)
            if entry:
                self.y_var.set(int(entry['y']))

    def on_canvas_release(self, event):
        self.dragging = False

    def highlight_selected_item(self):
        # Remove highlight from all items by setting normal font
        for item in self.canvas.find_withtag('text_item'):
            self.canvas.itemconfig(item, font=self.normal_font)
        if self.selected_item:
            # Highlight the selected item by setting bold font
            self.canvas.itemconfig(self.selected_item, font=self.bold_font)

    def delete_selected_item(self):
        # Remove the item from the canvas and data structures
        if not self.selected_item:
            return
        entry = self.get_entry_by_canvas_item(self.selected_item)
        if not entry:
            return
        # Remove from all pages
        for page in range(entry['start_page'], entry['end_page'] + 1):
            if page in self.pages and entry in self.pages[page]:
                self.pages[page].remove(entry)
        self.canvas.delete(self.selected_item)
        self.selected_item = None
        self.highlight_selected_item()
        self.disable_selected_item_controls()
        # Update character warning
        self.update_character_warning()

    def add_text_center(self):
        # Add text at the center of the canvas
        y = self.canvas_height / 2
        # Create a new text entry with default properties
        self.create_text_entry(y)

    def create_text_entry(self, y):
        # Set default properties
        text = "New Text"
        palette = "Red"
        start_page = self.current_page
        end_page = self.current_page
        multipage = False

        # Enforce rules
        if len(text) > 41:
            messagebox.showerror("Error", "Text exceeds 41 characters limit.")
            return
        if text.startswith(' ') or text.startswith('\0'):
            messagebox.showerror("Error", "Text cannot start with a space or null terminator.")
            return

        # Create the text item on the canvas, centered horizontally
        text_item = self.canvas.create_text(self.canvas_width / 2, y, text=text, anchor=tk.CENTER, fill=palette.lower(), tags=('text_item',), font=self.bold_font)

        # Add entry to pages
        entry = {
            'text': text,
            'y': y,
            'palette': palette,
            'multipage': multipage,
            'start_page': start_page,
            'end_page': end_page,
            'canvas_item': text_item
        }

        self.update_entry_pages(entry)

        # Select the new text item
        self.selected_item = text_item
        self.highlight_selected_item()
        self.update_selected_item_controls()

        # Refresh canvas to show the new text
        self.refresh_canvas()

    def update_entry_pages(self, entry):
        # Remove entry from all pages first
        for page_entries in self.pages.values():
            if entry in page_entries:
                page_entries.remove(entry)
        # Add entry to specified pages
        for page in range(entry['start_page'], entry['end_page'] + 1):
            if page not in self.pages:
                self.pages[page] = []
            self.pages[page].append(entry)

    def update_entry_position(self, canvas_item, dy):
        # Update the position of the entry in the data structure
        entry = self.get_entry_by_canvas_item(canvas_item)
        if not entry:
            return
        entry['y'] += dy

    def get_entry_by_canvas_item(self, canvas_item):
        # Find the entry corresponding to the canvas item
        for page_entries in self.pages.values():
            for entry in page_entries:
                if entry.get('canvas_item') == canvas_item:
                    return entry
        return None

    def prev_page(self, event=None):
        if self.current_page > 0:
            self.current_page -= 1
            self.page_label.config(text=f"Page: {self.current_page}")
            self.refresh_canvas()

    def next_page(self, event=None):
        self.current_page += 1
        self.page_label.config(text=f"Page: {self.current_page}")
        self.refresh_canvas()

    def draw_guides(self):
        if not self.show_guides.get():
            return

        color = self.guide_color.get()
        num_v_lines = self.vertical_lines.get()
        num_h_lines = self.horizontal_lines.get()

        # Draw vertical lines
        if num_v_lines > 0:
            v_spacing = self.canvas_width / (num_v_lines + 1)
            for i in range(1, num_v_lines + 1):
                x = v_spacing * i
                self.canvas.create_line(x, 0, x, self.canvas_height, fill=color, dash=(2, 4))

        # Draw horizontal lines
        if num_h_lines > 0:
            h_spacing = self.canvas_height / (num_h_lines + 1)
            for i in range(1, num_h_lines + 1):
                y = h_spacing * i
                self.canvas.create_line(0, y, self.canvas_width, y, fill=color, dash=(2, 4))

    def refresh_canvas(self):
        self.canvas.delete("all")
        if self.current_page in self.pages:
            entries = self.pages[self.current_page]
            # Sort entries by Y-value
            entries = sorted(entries, key=lambda e: e['y'])
            for entry in entries:
                # Determine font based on selection
                font = self.bold_font if self.selected_item and self.selected_item == entry.get('canvas_item') else self.normal_font
                text_item = self.canvas.create_text(self.canvas_width / 2, entry['y'], text=entry['text'],
                                                    anchor=tk.CENTER, fill=entry['palette'].lower(), tags=('text_item',), font=font)
                # Keep track of the canvas item
                entry['canvas_item'] = text_item
                # Update selected_item reference if necessary
                if self.selected_item == entry.get('canvas_item'):
                    self.selected_item = text_item
        # Draw guide lines if enabled
        self.draw_guides()
        self.highlight_selected_item()
        # Update character warning
        self.update_character_warning()

    def calculate_total_chars(self, page):
        if page not in self.pages:
            return 0
        total = 0
        for entry in self.pages[page]:
            total += len(entry['text'].replace(' ', ''))
        return total

    def update_character_warning(self):
        total_chars = self.calculate_total_chars(self.current_page)
        if total_chars > 128:
            self.warning_label.config(text=f"Warning: Total characters on page exceed 128 (Current: {total_chars})")
        else:
            self.warning_label.config(text="")

    def generate_cpp_code(self):
        code_lines = []
        code_lines.append('#include "nsmb.hpp"\n')
        code_lines.append('\n')
        code_lines.append('ncp_over(0x020EA678, 8)')
        code_lines.append('static constexpr End::ScriptEntry script[] = {\n')

        entries_processed = set()

        # Collect entries, sorted by page number
        pages = sorted(self.pages.keys())
        for page_num in pages:
            page_entries = self.pages[page_num]
            entries_to_generate = []
            # Collect entries that are on this page
            for entry in page_entries:
                key = (entry['text'], entry['y'], entry['start_page'], entry['end_page'])
                if key not in entries_processed:
                    entries_processed.add(key)
                    if entry['multipage']:
                        # Generate an entry for each page in the multipage range
                        for page in range(entry['start_page'], entry['end_page'] + 1):
                            if page not in self.pages:
                                continue
                            if page == page_num:
                                entries_to_generate.append((entry, page))
                    else:
                        if entry['start_page'] == page_num:
                            entries_to_generate.append((entry, page_num))
                else:
                    if entry['multipage'] and entry['start_page'] <= page_num <= entry['end_page']:
                        entries_to_generate.append((entry, page_num))

            if not entries_to_generate:
                continue  # Skip pages with no entries

            code_lines.append(f'\t// Page {page_num}')
            # Sort entries by Y-value
            sorted_entries = sorted(entries_to_generate, key=lambda e: e[0]['y'])
            for entry, page in sorted_entries:
                is_multipage = 'true' if (entry['multipage'] and page < entry['end_page']) else 'false'
                code_lines.append(f'\t{{"{entry["text"]}"end, {page}, {int(entry["y"])}, End::ScriptEntry::{entry["palette"]}, {is_multipage}}},')

        code_lines.append('\tEnd::ScriptTerminator, // Terminator')
        code_lines.append('};\n')
        code_lines.append('static_assert(NTR_ARRAY_SIZE(script) < 115, "Script size out of bounds");')

        # Show the generated code in a new window
        code_window = tk.Toplevel(self.root)
        code_window.title("Generated CPP Code")
        code_text = tk.Text(code_window, wrap='none')
        code_text.pack(expand=True, fill='both')
        code_text.insert('1.0', '\n'.join(code_lines))

        # Add a scrollbar
        scroll_x = tk.Scrollbar(code_window, orient='horizontal', command=code_text.xview)
        scroll_x.pack(side='bottom', fill='x')
        code_text.configure(xscrollcommand=scroll_x.set)

        scroll_y = tk.Scrollbar(code_window, orient='vertical', command=code_text.yview)
        scroll_y.pack(side='right', fill='y')
        code_text.configure(yscrollcommand=scroll_y.set)


    def save_data(self):
        file_path = filedialog.asksaveasfilename(defaultextension=".json",
                                                 filetypes=[("JSON files", "*.json")])
        if file_path:
            # Convert entries to serializable format
            serializable_pages = {}
            for page, entries in self.pages.items():
                serializable_entries = []
                for entry in entries:
                    serializable_entry = entry.copy()
                    if 'canvas_item' in serializable_entry:
                        del serializable_entry['canvas_item']  # Remove non-serializable item
                    serializable_entries.append(serializable_entry)
                serializable_pages[page] = serializable_entries
            with open(file_path, 'w') as f:
                json.dump(serializable_pages, f, indent=4)

    def load_data(self):
        file_path = filedialog.askopenfilename(filetypes=[("JSON files", "*.json")])
        if file_path:
            with open(file_path, 'r') as f:
                loaded_pages = json.load(f)
            # Convert page keys to integers
            self.pages = {}
            for page_str, entries in loaded_pages.items():
                page_num = int(page_str)
                self.pages[page_num] = []
                for entry in entries:
                    # Ensure numeric fields are correct types
                    entry['y'] = float(entry['y'])
                    entry['start_page'] = int(entry['start_page'])
                    entry['end_page'] = int(entry['end_page'])
                    entry['multipage'] = bool(entry['multipage'])
                    entry['canvas_item'] = None  # Will be set during refresh
                    self.pages[page_num].append(entry)
            # Clear current selection and refresh canvas
            self.selected_item = None
            self.disable_selected_item_controls()
            self.refresh_canvas()

# Run the app
if __name__ == '__main__':
    root = tk.Tk()
    app = CreditsApp(root)
    root.mainloop()
