import tkinter as tk
from tkinter import ttk
from tkinter.scrolledtext import ScrolledText
from pathlib import Path
from typing import Optional

try:
    from pygments import lex
    from pygments.lexers import get_lexer_for_filename, TextLexer
    from pygments.token import Token
    _HAS_PYGMENTS = True
except Exception:
    _HAS_PYGMENTS = False


class FileViewer(tk.Toplevel):
    """Toplevel window to show file contents with optional syntax highlighting
    and a simple minimap indicating code/comment/blank lines.
    """

    COLORS = {
        "code": "#1f77b4",
        "comment": "#2ca02c",
        "blank": "#d3d3d3",
    }

    def __init__(self, parent, path: str):
        super().__init__(parent)
        self.title(f"Viewer - {Path(path).name}")
        self.geometry("900x600")
        self.path = Path(path)

        # Main panes: Text on the left, minimap on the right
        main = ttk.Frame(self)
        main.pack(fill="both", expand=True)

        text_frame = ttk.Frame(main)
        text_frame.pack(side="left", fill="both", expand=True)

        self.text = ScrolledText(text_frame, wrap="none", undo=True)
        self.text.pack(fill="both", expand=True)
        self.text.config(state="disabled")

        minimap_frame = ttk.Frame(main, width=120)
        minimap_frame.pack(side="right", fill="y")
        self.canvas = tk.Canvas(minimap_frame, width=120, bg="#ffffff")
        self.canvas.pack(fill="y", expand=True)

        # Legend under the minimap showing color mapping
        legend_frame = ttk.Frame(minimap_frame)
        legend_frame.pack(side="bottom", fill="x", padx=4, pady=4)

        # Ordered legend entries
        for key, label_text in (('code', 'Code'), ('comment', 'Comment'), ('blank', 'Blank')):
            color = self.COLORS.get(key, '#cccccc')
            sw = tk.Label(legend_frame, bg=color, width=2, relief='ridge')
            sw.pack(side='left', padx=(2, 4))
            lbl = ttk.Label(legend_frame, text=label_text)
            lbl.pack(side='left', padx=(0, 8))

        self._load_file()

    def _load_file(self):
        try:
            text = self.path.read_text(errors="ignore")
        except Exception as e:
            self._set_text(f"Error opening file: {e}")
            return

        # Insert text
        self._set_text(text)

        # Apply highlighting
        if _HAS_PYGMENTS:
            self._apply_pygments_highlighting(text)
        else:
            self._apply_simple_highlighting()

        # Build minimap
        self._build_minimap(text.splitlines())

        # start periodic viewport updater
        self._viewport_job = None
        self._schedule_viewport_update()
        # cancel the job when window is closed
        self.bind("<Destroy>", lambda e: self._cancel_viewport_update())

    def _set_text(self, text: str):
        self.text.config(state="normal")
        self.text.delete("1.0", "end")
        self.text.insert("1.0", text)
        self.text.config(state="disabled")

    def _apply_pygments_highlighting(self, text: str):
        for tag in list(self.text.tag_names()):
            self.text.tag_delete(tag)

        try:
            lexer = get_lexer_for_filename(str(self.path))
        except Exception:
            lexer = TextLexer()

        token_map = {
            Token.Comment: "comment",
            Token.String: "string",
            Token.Keyword: "keyword",
            Token.Name: "name",
        }

        self.text.tag_configure("comment", foreground="#2ca02c")
        self.text.tag_configure("keyword", foreground="#d62728")
        self.text.tag_configure("string", foreground="#9467bd")

        pos = 0
        for ttype, value in lex(text, lexer):
            length = len(value)
            if length == 0:
                continue
            start_idx = self._index_from_pos(pos)
            end_idx = self._index_from_pos(pos + length)
            tag = None
            for key, tname in token_map.items():
                if ttype in key:
                    tag = tname
                    break
            if tag:
                try:
                    self.text.tag_add(tag, start_idx, end_idx)
                except Exception:
                    pass
            pos += length

    def _index_from_pos(self, pos: int) -> str:
        return f"1.0 + {pos} chars"

    def _apply_simple_highlighting(self):
        self.text.tag_configure("comment", foreground=self.COLORS["comment"]) 
        self.text.config(state="normal")
        for i, line in enumerate(self.text.get("1.0", "end").splitlines(), start=1):
            s = line.lstrip()
            if not s:
                continue
            if s.startswith("#") or s.startswith("//") or s.startswith("/*"):
                start = f"{i}.0"
                end = f"{i}.end"
                self.text.tag_add("comment", start, end)
        self.text.config(state="disabled")

    def _build_minimap(self, lines):
        self.canvas.delete("all")
        h = max(1, len(lines))
        width = int(self.canvas.winfo_reqwidth()) or 120
        # determine per-line rectangle height based on canvas height and number of lines
        canvas_h = max(1, self.canvas.winfo_height() or 600)
        rect_h = max(1, int(max(2, min(8, canvas_h / h))))
        y = 0
        # store for viewport computations
        self._minimap_line_height = rect_h
        self._minimap_total_lines = h
        for line in lines:
            typ = "code"
            s = line.strip()
            if not s:
                typ = "blank"
            elif s.startswith("#") or s.startswith("//") or s.startswith("/*"):
                typ = "comment"
            color = self.COLORS.get(typ, "#cccccc")
            self.canvas.create_rectangle(2, y, width-2, y+rect_h, fill=color, outline=color)
            y += rect_h + 1
        # create viewport rectangle overlay (on top)
        # remove any existing viewport and create a fresh one
        try:
            self.canvas.delete("viewport")
        except Exception:
            pass
        self.canvas.create_rectangle(0, 0, 0, 0, outline="#ff0000", width=1, tags=("viewport",))
        # if canvas size changes, rebuild minimap
        self.canvas.bind("<Configure>", lambda e: self._build_minimap(lines))

    def _schedule_viewport_update(self, interval_ms: int = 200):
        # schedule periodic viewport updates
        def _job():
            try:
                self._update_viewport_rect()
            finally:
                self._viewport_job = self.after(interval_ms, _job)

        # cancel any existing job then start
        self._cancel_viewport_update()
        self._viewport_job = self.after(interval_ms, _job)

    def _cancel_viewport_update(self):
        if getattr(self, "_viewport_job", None):
            try:
                self.after_cancel(self._viewport_job)
            except Exception:
                pass
            self._viewport_job = None

    def _update_viewport_rect(self):
        # get top/bottom fractions from text widget
        try:
            top, bottom = self.text.yview()
        except Exception:
            return
        c_h = self.canvas.winfo_height() or 1
        y1 = int(top * c_h)
        y2 = int(bottom * c_h)
        # ensure reasonable bounds
        y1 = max(0, min(y1, c_h))
        y2 = max(0, min(y2, c_h))
        # update the viewport rectangle
        try:
            self.canvas.coords("viewport", 1, y1, self.canvas.winfo_width()-1, y2)
        except Exception:
            pass
