# ui/main_window.py — main window construction and application entry point

import queue
import tkinter as tk
import tkinter.ttk as ttk
import tkinter.filedialog as fd
import lionscliapp as app

from librarian2 import state as st
from librarian2 import dispatch as d
from librarian2.ui import theme
from librarian2.ui.index_pane  import build_index_pane,  refresh_index
from librarian2.ui.editor_pane import build_editor_pane, refresh_editor
from librarian2.ui.status_bar  import build_status_bar,  refresh_status_bar
from librarian2.ui.menus       import build_menu_bar


def run_editor():
    """Entry point for the GUI. Called by lionscliapp after ctx is ready.

    Initializes state, wires up dispatch, builds the main window,
    optionally loads the configured registry, then enters the main loop.
    """
    st.init_state()
    d._setup()

    root = tk.Tk()
    root.title("Lion's Librarian - Registry Editor")
    root.geometry('800x600')
    root.withdraw()                  # hide until layout is complete
    root.option_add('*tearOff', False)

    st.g[st.TK]    = root
    st.g[st.QUEUE] = queue.Queue()

    theme.apply_theme(root)
    build_main_window(root)
    bind_keys(root)

    from librarian2 import patchboard
    patchboard.announce_self()
    patchboard.start_input_polling(root)

    registry_path = app.ctx.get('path.registry')
    if not (registry_path and registry_path.is_file()):
        registry_path = app.get_path('registry.json', 'p')

    if registry_path.is_file():
        d.dispatch(d.LOAD_REGISTRY, registry_path)
        refresh_all(st.g)
    else:
        # No registry yet — point state at the default path so Save works.
        st.g[st.REG_PATH] = registry_path

    load_window_geometry(root)
    root.deiconify()
    bind_geometry_saves(root)
    root.mainloop()


def build_main_window(root):
    """Construct the two-pane layout inside root.

    Left pane: entry index.   Right pane: entry editor.
    A draggable sash separates the two; status bar is pinned at the bottom.
    """
    widgets = st.g[st.WIDGETS]

    root.columnconfigure(0, weight=1)
    root.rowconfigure(0, weight=1)
    root.rowconfigure(1, weight=0)

    build_menu_bar(root, widgets)

    paned = ttk.PanedWindow(root, orient='horizontal')

    index_pane  = build_index_pane(paned, widgets)
    editor_pane = build_editor_pane(paned, widgets)
    status_bar  = build_status_bar(root, widgets)

    paned.add(index_pane,  weight=0)
    paned.add(editor_pane, weight=1)

    paned.grid(     row=0, column=0, sticky='nsew')
    status_bar.grid(row=1, column=0, sticky='ew')

    widgets['paned'] = paned


def bind_keys(root):
    """Bind global keyboard shortcuts to the root window."""
    from librarian2.ui.menus import cmd_open_registry, cmd_raise_entry, cmd_lower_entry
    root.bind('<Control-o>',    lambda e: cmd_open_registry())
    root.bind('<Control-s>',    lambda e: _save())
    root.bind('<Control-q>',    lambda e: root.destroy())
    root.bind('<Control-j>',    lambda e: _toggle_editor_mode())
    root.bind('<Control-Return>', lambda e: _cmd_apply())
    root.bind('<Control-Up>',   lambda e: cmd_raise_entry())
    root.bind('<Control-Down>', lambda e: cmd_lower_entry())
    root.bind('<Control-e>',    lambda e: d.dispatch(d.PATCHBOARD_EMIT))


def _toggle_editor_mode():
    from librarian2.ui.menus import _set_editor_mode
    new_mode = 'raw' if st.g[st.EDITOR_MODE] == 'form' else 'form'
    _set_editor_mode(new_mode)


def _cmd_apply():
    from librarian2.ui.menus import cmd_apply
    cmd_apply()


def refresh_all(g):
    """Refresh all UI components from current application state.

    Preserves keyboard focus across the rebuild: finds which widgets-dict
    key held focus before, then restores focus to the same key after.
    """
    focused_key = _focused_widget_key(g)

    refresh_title(g)
    refresh_index(g)
    refresh_editor(g)
    refresh_status_bar(g)

    _restore_focus(g, focused_key)


def refresh_title(g):
    """Set the window title to reflect the currently open registry path."""
    reg_path = g.get(st.REG_PATH)
    if reg_path:
        title = f"Lion's Librarian - {reg_path}"
    else:
        title = "Lion's Librarian - Registry Editor"
    g[st.TK].title(title)


def _focused_widget_key(g):
    """Return the widgets-dict key of the currently focused widget, or None."""
    focused = g[st.TK].focus_get()
    if focused is None:
        return None
    for key, w in g[st.WIDGETS].items():
        if callable(w):
            continue
        if w is focused:
            return key
    return None


def _restore_focus(g, key):
    """Focus the widget stored under key in the widgets dict, if it still exists."""
    if not key:
        return
    w = g[st.WIDGETS].get(key)
    if w is None or callable(w):
        return
    try:
        w.focus_set()
    except Exception:
        pass


def _save():
    d.dispatch(d.SAVE_REGISTRY)
    refresh_all(st.g)


# ---------------------------------------------------------------------------
# Window geometry persistence  (.librarian2/window.json)
# ---------------------------------------------------------------------------

_geometry_save_id   = None   # after() handle for debouncing Configure events
_last_known_sash    = None   # last valid sash value; fallback when paned reports 0
_geometry_ready     = False  # blocks saves until startup is complete


def load_window_geometry(root):
    """Read window.json and apply saved width, height, and sash position."""
    global _last_known_sash
    try:
        path = app.get_path('window.json', 'p')
        data = app.read_json('window.json', 'p')
        print(f'[window.json] READ   sash={data.get("sash")}  ← {path}')
    except FileNotFoundError:
        print('[window.json] READ   not found — using defaults')
        _geometry_ready = True
        return
    w = int(data.get('width',  800))
    h = int(data.get('height', 600))
    root.geometry(f'{w}x{h}')
    sash = data.get('sash')
    if sash is not None:
        _last_known_sash = int(sash)
        paned = st.g[st.WIDGETS].get('paned')
        if paned:
            # Apply sash via paned's own <Configure>, which fires once it has
            # a real allocated width — the only reliable trigger on all platforms.
            _target = int(sash)
            def _on_paned_ready(event, _t=_target):
                global _geometry_ready
                if event.width > 0:
                    paned.unbind('<Configure>')
                    paned.sashpos(0, _t)
                    print(f'[window.json] APPLY  sashpos(0, {_t}) → actual={paned.sashpos(0)}')
                    _geometry_ready = True
            paned.bind('<Configure>', _on_paned_ready)
    else:
        _geometry_ready = True


def save_window_geometry(root):
    """Write current width, height, and sash position to window.json."""
    global _last_known_sash
    if not _geometry_ready:
        return
    paned    = st.g[st.WIDGETS].get('paned')
    raw_sash = paned.sashpos(0) if paned else 0
    if raw_sash > 0:
        _last_known_sash = raw_sash
    sash = _last_known_sash or 0
    data = {
        'width':  root.winfo_width(),
        'height': root.winfo_height(),
        'sash':   sash,
    }
    try:
        path = app.get_path('window.json', 'p')
        print(f'[window.json] WRITE  sash={sash} (raw={raw_sash})  → {path}')
        app.write_json('window.json', data, 'p2')
    except Exception:
        pass


def bind_geometry_saves(root):
    """Bind resize and sash-drag events to save window geometry."""
    # Debounce Configure so we don't write on every pixel of resize
    root.bind('<Configure>',
              lambda e: _on_root_configure(e, root))
    paned = st.g[st.WIDGETS].get('paned')
    if paned:
        paned.bind('<ButtonRelease-1>', lambda e: save_window_geometry(root))


def _on_root_configure(event, root):
    global _geometry_save_id
    if event.widget is not root:
        return
    if _geometry_save_id is not None:
        root.after_cancel(_geometry_save_id)
    _geometry_save_id = root.after(300, lambda: save_window_geometry(root))
