"""Journal — a tiny desktop journal.

Drop this file anywhere and double-click it (Python 3 required). It opens
on a setup screen: your name, the journal's name, and style choices (font,
colors, border). "Open journal" takes you to the writing screen.

Entries are stamped with date + time (and your name) and appended to
journal\\<journal-name>-001.md next to this script. Once the active file
reaches MAX_FILE_KB, the next save starts -002, and so on. Your name,
journal name, and style are remembered in journal-settings.json.

Your words are plain markdown files on disk — no accounts, no cloud,
yours to keep.
"""
from __future__ import annotations

import datetime as dt
import json
import os
import re

# ---- knobs --------------------------------------------------------------
MAX_FILE_KB = 10            # roll to a new file once the active one hits this
FONT_SIZE = 13              # journal text size; the rest of the UI scales off it
JOURNAL_DIRNAME = "journal" # folder created next to this script
SETTINGS_FILE = "journal-settings.json"

# ---- style catalog ------------------------------------------------------
THEMES = {
    "Ember":   dict(BG="#0b0b12", PANEL="#12121d", FIELD="#181826", FG="#e9e7f2",
                    DIM="#77758f", MUTED="#3c3b52", ACCENT="#f5731d", ACCENT_HI="#ff8a3d"),
    "Ocean":   dict(BG="#081018", PANEL="#0d1822", FIELD="#12202e", FG="#e6eef5",
                    DIM="#6f8496", MUTED="#33465a", ACCENT="#38b6ff", ACCENT_HI="#6cc9ff"),
    "Forest":  dict(BG="#0a120c", PANEL="#101a12", FIELD="#152218", FG="#e8f0e8",
                    DIM="#7b917e", MUTED="#3a4f3e", ACCENT="#4caf6d", ACCENT_HI="#6fcf8e"),
    "Violet":  dict(BG="#0e0a16", PANEL="#151020", FIELD="#1c1530", FG="#ece7f5",
                    DIM="#8d82a8", MUTED="#463a63", ACCENT="#a78bfa", ACCENT_HI="#c4b0ff"),
    "Rose":    dict(BG="#140a0e", PANEL="#1c1016", FIELD="#241420", FG="#f5e7ed",
                    DIM="#a07f90", MUTED="#55374a", ACCENT="#f472b6", ACCENT_HI="#ff9ccb"),
    "Gold":    dict(BG="#12100a", PANEL="#1a1710", FIELD="#221e14", FG="#f2edda",
                    DIM="#998f6f", MUTED="#4f4930", ACCENT="#eab308", ACCENT_HI="#facc15"),
    "Crimson": dict(BG="#130a0a", PANEL="#1c1010", FIELD="#241414", FG="#f2e7e7",
                    DIM="#a07f7f", MUTED="#553737", ACCENT="#ef4444", ACCENT_HI="#f87171"),
    "Teal":    dict(BG="#081412", PANEL="#0d1c1a", FIELD="#122422", FG="#e6f2f0",
                    DIM="#6f9691", MUTED="#33544f", ACCENT="#2dd4bf", ACCENT_HI="#5eead4"),
    "Sky":     dict(BG="#0e1116", PANEL="#151a21", FIELD="#1c222b", FG="#e8edf5",
                    DIM="#8b96a8", MUTED="#414c5e", ACCENT="#60a5fa", ACCENT_HI="#93c5fd"),
    "Slate":   dict(BG="#0f0f11", PANEL="#17171a", FIELD="#1f1f23", FG="#ececf0",
                    DIM="#8b8b95", MUTED="#45454f", ACCENT="#c8c8d2", ACCENT_HI="#e2e2ea"),
    "Paper":   dict(BG="#efe9dd", PANEL="#f8f4ea", FIELD="#fffdf7", FG="#2b2620",
                    DIM="#8b8272", MUTED="#cfc6b2", ACCENT="#b4551d", ACCENT_HI="#d2691e"),
    "Mint":    dict(BG="#e9f2ec", PANEL="#f3faf5", FIELD="#fcfffd", FG="#1f2b24",
                    DIM="#6f8a7a", MUTED="#b7d1c2", ACCENT="#157f4f", ACCENT_HI="#1a9d62"),
    "Blush":   dict(BG="#f6ecef", PANEL="#fbf4f6", FIELD="#fffbfc", FG="#33232a",
                    DIM="#99757f", MUTED="#dcc0c9", ACCENT="#c2417a", ACCENT_HI="#d95a92"),
    "Sea":     dict(BG="#e9eff5", PANEL="#f3f7fb", FIELD="#fbfdff", FG="#202a33",
                    DIM="#6f8496", MUTED="#b8ccdc", ACCENT="#1868b0", ACCENT_HI="#1f7fd4"),
}
FONTS = ["Segoe UI", "Arial", "Calibri", "Candara", "Trebuchet MS", "Verdana",
         "Tahoma", "Bahnschrift", "Georgia", "Palatino Linotype", "Garamond",
         "Cambria", "Constantia", "Times New Roman", "Consolas", "Courier New",
         "Comic Sans MS", "Segoe Script"]
BORDERS = ["None", "Thin", "Bold"]
DEFAULTS = {"name": "", "journal": "Journal",
            "font": "Segoe UI", "theme": "Ember", "border": "None"}

BASE = os.path.dirname(os.path.abspath(__file__))
JDIR = os.path.join(BASE, JOURNAL_DIRNAME)
SPATH = os.path.join(BASE, SETTINGS_FILE)


# ---- settings -----------------------------------------------------------
def load_settings() -> dict:
    s = dict(DEFAULTS)
    try:
        with open(SPATH, encoding="utf-8") as f:
            s.update({k: v for k, v in json.load(f).items() if k in DEFAULTS})
    except Exception:
        pass
    if s["theme"] not in THEMES:
        s["theme"] = DEFAULTS["theme"]
    if s["border"] not in BORDERS:
        s["border"] = DEFAULTS["border"]
    if s["font"] not in FONTS:
        s["font"] = DEFAULTS["font"]
    return s


def save_settings(s: dict) -> None:
    try:
        with open(SPATH, "w", encoding="utf-8") as f:
            json.dump(s, f, indent=2)
    except OSError:
        pass


# ---- file logic (no GUI in here, so it's testable) ----------------------
def slugify(name: str) -> str:
    slug = re.sub(r"[^a-z0-9]+", "-", name.lower()).strip("-")
    return slug or "journal"


def _numbered(slug: str) -> list[tuple[int, str]]:
    """All files for this journal as (number, full_path), sorted numerically."""
    os.makedirs(JDIR, exist_ok=True)
    pat = re.compile(rf"{re.escape(slug)}-(\d+)\.md$")
    out = []
    for name in os.listdir(JDIR):
        m = pat.fullmatch(name)
        if m:
            out.append((int(m.group(1)), os.path.join(JDIR, name)))
    return sorted(out)


def active_file(slug: str) -> str:
    """Path that the next entry should append to (rolls over at the cap)."""
    files = _numbered(slug)
    if not files:
        return os.path.join(JDIR, f"{slug}-001.md")
    num, latest = files[-1]
    if os.path.getsize(latest) >= MAX_FILE_KB * 1024:
        return os.path.join(JDIR, f"{slug}-{num + 1:03d}.md")
    return latest


def append_entry(slug: str, text: str, author: str = "", title: str = "") -> str:
    """Timestamp + append the entry; returns the path it landed in."""
    path = active_file(slug)
    fresh = not os.path.exists(path)
    now = dt.datetime.now()
    stamp = now.strftime("%Y-%m-%d %H:%M") + (f" — {author}" if author else "")
    with open(path, "a", encoding="utf-8", newline="\n") as f:
        if fresh:
            f.write(f"# {title or slug} — started {now.strftime('%Y-%m-%d')}\n\n")
        f.write(f"## {stamp}\n{text.strip()}\n\n")
    return path


# ---- GUI ----------------------------------------------------------------
def launch():
    import subprocess
    import tkinter as tk

    try:                                    # crisp text on scaled displays
        from ctypes import windll
        windll.shcore.SetProcessDpiAwareness(1)
    except Exception:
        pass

    root = tk.Tk()
    root.title("Journal")
    s = max(1.0, root.winfo_fpixels("1i") / 96.0)   # respect display scaling
    root.geometry(f"{int(660 * s)}x{int(640 * s)}")
    root.minsize(int(540 * s), int(430 * s))

    settings = load_settings()
    state = {"mode": "welcome", "viewing": None}

    container = tk.Frame(root)
    container.pack(fill="both", expand=True)

    def T() -> dict:
        return THEMES[settings["theme"]]

    def fonts():
        fam = settings["font"]
        return {
            "body":  (fam, FONT_SIZE),
            "stamp": (fam, FONT_SIZE - 1, "bold"),
            "small": (fam, FONT_SIZE - 3),
            "btn":   (fam, FONT_SIZE - 2),
            "btnb":  (fam, FONT_SIZE - 2, "bold"),
            "title": (fam, FONT_SIZE + 8, "bold"),
            "label": (fam, FONT_SIZE - 2),
        }

    def border_kit():
        t = T()
        return {"None": dict(highlightthickness=0),
                "Thin": dict(highlightthickness=1, highlightbackground=t["MUTED"],
                             highlightcolor=t["MUTED"]),
                "Bold": dict(highlightthickness=2, highlightbackground=t["ACCENT"],
                             highlightcolor=t["ACCENT"])}[settings["border"]]

    def set_style(key, value):
        settings[key] = value
        save_settings(settings)
        rebuild()

    def rebuild():
        for w in container.winfo_children():
            w.destroy()
        (build_welcome if state["mode"] == "welcome" else build_journal)()

    # ---- screen one: setup ---------------------------------------------
    def build_welcome():
        t, F = T(), fonts()
        root.configure(bg=t["BG"])
        container.configure(bg=t["BG"])
        card = tk.Frame(container, bg=t["BG"])
        card.place(relx=0.5, rely=0.5, anchor="center")

        tk.Label(card, text="Journal", bg=t["BG"], fg=t["FG"],
                 font=F["title"]).grid(row=0, column=0, columnspan=2, pady=(0, 2))
        tk.Label(card, text=f"date-stamped entries · files roll at {MAX_FILE_KB} KB",
                 bg=t["BG"], fg=t["DIM"], font=F["small"]
                 ).grid(row=1, column=0, columnspan=2, pady=(0, 22))

        def lab(row, text, pady=(0, 10)):
            tk.Label(card, text=text, bg=t["BG"], fg=t["DIM"], font=F["label"],
                     anchor="e", width=12).grid(row=row, column=0, sticky="e",
                                               padx=(0, 12), pady=pady)

        def field(row, initial):
            e = tk.Entry(card, bg=t["FIELD"], fg=t["FG"], font=F["body"],
                         insertbackground=t["ACCENT"], relief="flat", width=24,
                         selectbackground=t["ACCENT"], selectforeground=t["BG"],
                         **border_kit())
            e.insert(0, initial)
            e.grid(row=row, column=1, sticky="w", ipady=5, pady=(0, 10))
            return e

        lab(2, "Your name");    name_e = field(2, settings["name"])
        lab(3, "Journal name"); jour_e = field(3, settings["journal"])

        tk.Label(card, text="S T Y L E", bg=t["BG"], fg=t["MUTED"],
                 font=F["small"]).grid(row=4, column=0, columnspan=2, pady=(10, 10))

        def dropdown(row, key, options):
            var = tk.StringVar(value=settings[key])
            opt = tk.OptionMenu(card, var, *options,
                                command=lambda v, k=key: set_style(k, v))
            opt.configure(bg=t["FIELD"], fg=t["FG"], activebackground=t["PANEL"],
                          activeforeground=t["ACCENT"], relief="flat", bd=0,
                          font=F["btn"], cursor="hand2", width=16,
                          highlightthickness=0, indicatoron=True)
            opt["menu"].configure(bg=t["FIELD"], fg=t["FG"], bd=0, font=F["btn"],
                                  activebackground=t["ACCENT"],
                                  activeforeground=t["BG"])
            opt.grid(row=row, column=1, sticky="w", pady=(0, 10))

        lab(5, "Font")
        fbtn = tk.Menubutton(card, text=settings["font"], bg=t["FIELD"],
                             fg=t["FG"], activebackground=t["PANEL"],
                             activeforeground=t["ACCENT"], relief="flat", bd=0,
                             font=(settings["font"], FONT_SIZE - 2),
                             cursor="hand2", width=18, anchor="w",
                             padx=10, pady=5, highlightthickness=0)
        fmenu = tk.Menu(fbtn, tearoff=0, bg=t["FIELD"], fg=t["FG"], bd=0,
                        activebackground=t["ACCENT"], activeforeground=t["BG"])
        for fname in FONTS:
            fmenu.add_command(label=fname, font=(fname, FONT_SIZE - 2),
                              command=lambda v=fname: set_style("font", v))
        fbtn.configure(menu=fmenu)
        fbtn.grid(row=5, column=1, sticky="w", pady=(0, 10))
        lab(6, "Colors")
        sw = tk.Frame(card, bg=t["BG"])
        sw.grid(row=6, column=1, sticky="w", pady=(0, 10))
        for i, tn in enumerate(THEMES):
            th = THEMES[tn]
            b = tk.Button(sw, text="\u25cf", font=("Segoe UI", 8),
                          bg=th["BG"], fg=th["ACCENT"],
                          activebackground=th["BG"],
                          activeforeground=th["ACCENT_HI"],
                          relief="flat", bd=0, cursor="hand2", width=2,
                          highlightthickness=2,
                          highlightbackground=t["FG"] if tn == settings["theme"]
                          else t["BG"],
                          command=lambda v=tn: set_style("theme", v))
            b.grid(row=i // 7, column=i % 7, padx=(0, 5), pady=(0, 5))
        lab(7, "Border"); dropdown(7, "border", BORDERS)

        def open_journal():
            settings["name"] = name_e.get().strip()
            settings["journal"] = jour_e.get().strip() or "Journal"
            save_settings(settings)
            state["mode"] = "journal"
            state["viewing"] = None
            rebuild()

        tk.Button(card, text="Open journal  →", command=open_journal,
                  bg=t["ACCENT"], fg=t["BG"], activebackground=t["ACCENT_HI"],
                  activeforeground=t["BG"], relief="flat", bd=0, font=F["btnb"],
                  padx=22, pady=8, cursor="hand2"
                  ).grid(row=8, column=0, columnspan=2, pady=(16, 4))
        tk.Label(card, text=f"entries save next to this app in \\{JOURNAL_DIRNAME}",
                 bg=t["BG"], fg=t["MUTED"], font=F["small"]
                 ).grid(row=9, column=0, columnspan=2)

        name_e.focus_set()
        root.bind("<Return>", lambda e: open_journal() if state["mode"] == "welcome" else None)

    # ---- screen two: the journal ---------------------------------------
    def build_journal():
        t, F = T(), fonts()
        root.bind("<Return>", lambda e: None)
        root.configure(bg=t["BG"])
        container.configure(bg=t["BG"])
        slug = slugify(settings["journal"])

        view = tk.Text(container, bg=t["PANEL"], fg=t["FG"], font=F["body"],
                       insertbackground=t["FG"], wrap="word", relief="flat",
                       padx=16, pady=14, state="disabled", height=10,
                       selectbackground=t["ACCENT"], selectforeground=t["BG"],
                       **border_kit())
        view.tag_configure("stamp", foreground=t["ACCENT"], font=F["stamp"],
                           spacing1=10)
        view.tag_configure("title", foreground=t["DIM"], font=F["small"])

        entry = tk.Text(container, bg=t["FIELD"], fg=t["FG"], font=F["body"],
                        insertbackground=t["ACCENT"], wrap="word", relief="flat",
                        padx=14, pady=10, height=5,
                        selectbackground=t["ACCENT"], selectforeground=t["BG"],
                        **border_kit())

        bar = tk.Frame(container, bg=t["BG"])

        def positions() -> list[str]:
            paths = [p for _, p in _numbered(slug)]
            ap = active_file(slug)
            if not paths or paths[-1] != ap:
                paths.append(ap)
            return paths

        def refresh(flash: str = ""):
            if not status.winfo_exists():
                return
            pos = positions()
            last = len(pos) - 1
            idx = last if state["viewing"] is None else min(state["viewing"], last)
            live = idx == last
            if live:
                state["viewing"] = None
            path = pos[idx]
            view.configure(state="normal")
            view.delete("1.0", "end")
            if os.path.exists(path):
                for line in open(path, encoding="utf-8"):
                    if line.startswith("## "):
                        view.insert("end", line[3:], "stamp")
                    elif line.startswith("# "):
                        view.insert("end", line[2:], "title")
                    else:
                        view.insert("end", line)
            else:
                view.insert("end", "New file — first entry starts it.\n", "title")
            view.configure(state="disabled")
            view.see("end")
            prev_btn.configure(state="normal" if idx > 0 else "disabled")
            next_btn.configure(state="normal" if not live else "disabled")
            size = os.path.getsize(path) if os.path.exists(path) else 0
            tag = "" if live else "  ·  history"
            note = f"   {flash}" if flash else ""
            status.configure(
                text=f"{os.path.basename(path)}  ·  {size / 1024:.1f} / {MAX_FILE_KB} KB{tag}{note}",
                fg=t["ACCENT"] if flash else t["DIM"])
            if flash:
                root.after(1600, refresh)

        def browse(step: int):
            pos = positions()
            last = len(pos) - 1
            idx = last if state["viewing"] is None else state["viewing"]
            idx = max(0, min(last, idx + step))
            state["viewing"] = None if idx == last else idx
            refresh()

        def open_folder():
            os.makedirs(JDIR, exist_ok=True)
            try:
                os.startfile(JDIR)                      # Windows
            except AttributeError:
                subprocess.Popen(["xdg-open", JDIR])    # everywhere else

        def go_setup():
            state["mode"] = "welcome"
            rebuild()

        def save(event=None):
            text = entry.get("1.0", "end").strip()
            if text:
                append_entry(slug, text, settings["name"], settings["journal"])
                entry.delete("1.0", "end")
                state["viewing"] = None
                refresh("saved ✓")
            return "break"

        def ghost(text, cmd, pad=12):
            return tk.Button(bar, text=text, command=cmd, bg=t["FIELD"],
                             fg=t["FG"], activebackground=t["PANEL"],
                             activeforeground=t["ACCENT"],
                             disabledforeground=t["MUTED"], relief="flat", bd=0,
                             font=F["btn"], padx=pad, pady=5, cursor="hand2")

        setup_btn = ghost("Setup", go_setup)
        prev_btn = ghost("‹", lambda: browse(-1))
        next_btn = ghost("›", lambda: browse(+1))
        open_btn = ghost("Open folder", open_folder)
        status = tk.Label(bar, bg=t["BG"], fg=t["DIM"], font=F["small"],
                          anchor="w")
        save_btn = tk.Button(bar, text="Save entry  ·  Ctrl+Enter", command=save,
                             bg=t["ACCENT"], fg=t["BG"],
                             activebackground=t["ACCENT_HI"],
                             activeforeground=t["BG"], relief="flat", bd=0,
                             font=F["btnb"], padx=14, pady=5, cursor="hand2")

        # bottom-up packing: if the window ever runs short, the reading pane
        # is what shrinks — the entry box and buttons can never be clipped
        bar.pack(side="bottom", fill="x", padx=10, pady=(8, 10))
        entry.pack(side="bottom", fill="x", padx=10)
        view.pack(side="top", fill="both", expand=True, padx=10, pady=(10, 6))
        setup_btn.pack(side="left", padx=(0, 8))
        prev_btn.pack(side="left")
        next_btn.pack(side="left", padx=(4, 8))
        open_btn.pack(side="left")
        status.pack(side="left", fill="x", expand=True, padx=10)
        save_btn.pack(side="right")

        entry.bind("<Control-Return>", save)
        refresh()
        entry.focus_set()

    rebuild()
    root.mainloop()


if __name__ == "__main__":
    launch()
