(** SX Native Browser -- renders s-expressions directly to pixels.

    A proof-of-concept desktop browser that parses .sx files and
    renders them using SDL2 + Cairo, with no HTML/CSS/JS engine. *)

open Tsdl
open Sx_native
open Sx_native_types

(* -- Helpers for SDL result handling -- *)

let sdl_ok = function
  | Ok v -> v
  | Error (`Msg e) -> failwith ("SDL error: " ^ e)

(* -- State -- *)

type app_state = {
  mutable current_url : string;
  mutable root : node;
  mutable needs_repaint : bool;
  mutable win_w : int;
  mutable win_h : int;
  mutable scroll_y : float;
}

(* -- Parse and build render tree -- *)

let load_content (state : app_state) (source : string) (cr : Cairo.context) =
  let values = Sx_parser.parse_all source in
  let navigate href =
    (* Simple navigation: if href starts with / or is a relative path, reload *)
    Printf.printf "[navigate] %s\n%!" href;
    state.current_url <- href;
    (* In a full implementation, this would trigger a re-fetch and re-render *)
  in
  let root = Sx_native_render.render_page ~navigate values in
  Sx_native_layout.measure cr root;
  let w = float_of_int state.win_w in
  let h = float_of_int state.win_h -. 36. in  (* subtract URL bar *)
  Sx_native_layout.layout root 0. 0. w h;
  state.root <- root;
  state.needs_repaint <- true

(* -- Hit testing -- *)

let rec hit_test (node : node) (x : float) (y : float) : node option =
  let b = node.box in
  if x >= b.x && x <= b.x +. b.w && y >= b.y && y <= b.y +. b.h then begin
    (* Check children in reverse order (topmost first) *)
    let child_hit = List.fold_left (fun acc child ->
      match acc with
      | Some _ -> acc
      | None -> hit_test child x y
    ) None (List.rev node.children) in
    match child_hit with
    | Some _ -> child_hit
    | None -> Some node
  end
  else None

let handle_click (state : app_state) (root : node) (x : float) (y : float) =
  (* Offset y by URL bar height for hit testing *)
  let adjusted_y = y -. 36. -. state.scroll_y in
  match hit_test root x adjusted_y with
  | Some node ->
    (match node.on_click with
     | Some f -> f ()
     | None ->
       match node.href with
       | Some href ->
         Printf.printf "[click] link: %s\n%!" href;
         state.current_url <- href
       | None ->
         Printf.printf "[click] %s at (%.0f, %.0f)\n%!" node.tag x y)
  | None ->
    Printf.printf "[click] miss at (%.0f, %.0f)\n%!" x y

(* -- Default demo content -- *)

let demo_sx = {|
(div :class "flex flex-col items-center gap-6 p-8 bg-stone-50"
  (h1 :class "text-3xl font-bold text-stone-800" "SX Native Browser")
  (p :class "text-stone-500" "Rendering s-expressions directly to pixels")
  (div :class "flex gap-4 items-center"
    (div :class "p-4 rounded-lg bg-white border border-stone-200 shadow"
      (h3 :class "font-bold text-stone-700" "No HTML")
      (p :class "text-sm text-stone-500" "This is not a web page"))
    (div :class "p-4 rounded-lg bg-white border border-stone-200 shadow"
      (h3 :class "font-bold text-stone-700" "No CSS")
      (p :class "text-sm text-stone-500" "Tailwind classes parsed to native styles"))
    (div :class "p-4 rounded-lg bg-white border border-stone-200 shadow"
      (h3 :class "font-bold text-stone-700" "No JavaScript")
      (p :class "text-sm text-stone-500" "The SX evaluator does everything")))
  (div :class "p-6 rounded-lg bg-violet-600"
    (p :class "text-white text-lg font-bold" "5,000 lines of OCaml instead of 35 million lines of browser engine")))
|}

(* -- Main -- *)

let () =
  (* Parse command line *)
  let source = ref "" in
  let url = ref "sx://demo" in
  let args = Array.to_list Sys.argv in
  (match args with
   | _ :: file :: _ when Sys.file_exists file ->
     source := Sx_native_fetch.read_file file;
     url := "file://" ^ file
   | _ :: path :: _ when String.length path > 0 ->
     (try
        source := Sx_native_fetch.fetch_page path;
        url := path
      with _ ->
        Printf.eprintf "Failed to fetch %s, using demo content\n%!" path;
        source := demo_sx;
        url := "sx://demo")
   | _ ->
     source := demo_sx);

  (* Initialize SDL2 *)
  sdl_ok (Sdl.init Sdl.Init.(video + events));
  at_exit Sdl.quit;

  let initial_w = 1024 in
  let initial_h = 768 in

  let window = sdl_ok (Sdl.create_window "SX Browser"
    ~x:Sdl.Window.pos_centered ~y:Sdl.Window.pos_centered
    ~w:initial_w ~h:initial_h
    Sdl.Window.(shown + resizable + allow_highdpi)) in

  let renderer = sdl_ok (Sdl.create_renderer window
    ~flags:Sdl.Renderer.(accelerated + presentvsync)) in

  (* Create SDL texture for Cairo to draw into *)
  let create_texture w h =
    sdl_ok (Sdl.create_texture renderer Sdl.Pixel.format_argb8888
      Sdl.Texture.access_streaming ~w ~h)
  in
  let texture = ref (create_texture initial_w initial_h) in

  (* Create Cairo surface *)
  let create_cairo_surface w h =
    Cairo.Image.create Cairo.Image.ARGB32 ~w ~h
  in
  let surface = ref (create_cairo_surface initial_w initial_h) in
  let cr = ref (Cairo.create !surface) in

  (* App state *)
  let state = {
    current_url = !url;
    root = { tag = "root"; style = default_style; children = [];
             text = None; box = make_box (); href = None; on_click = None };
    needs_repaint = true;
    win_w = initial_w;
    win_h = initial_h;
    scroll_y = 0.;
  } in

  (* Initial load *)
  load_content state !source !cr;

  (* Main event loop *)
  let event = Sdl.Event.create () in
  let running = ref true in
  while !running do
    (* Process all pending events *)
    while Sdl.poll_event (Some event) do
      let typ = Sdl.Event.get event Sdl.Event.typ in

      if typ = Sdl.Event.quit then
        running := false

      else if typ = Sdl.Event.key_down then begin
        let scancode = Sdl.Event.get event Sdl.Event.keyboard_scancode in
        if scancode = Sdl.Scancode.escape then
          running := false
        else if scancode = Sdl.Scancode.up then begin
          state.scroll_y <- Float.min 0. (state.scroll_y +. 40.);
          state.needs_repaint <- true
        end
        else if scancode = Sdl.Scancode.down then begin
          state.scroll_y <- state.scroll_y -. 40.;
          state.needs_repaint <- true
        end
        else if scancode = Sdl.Scancode.home then begin
          state.scroll_y <- 0.;
          state.needs_repaint <- true
        end
      end

      else if typ = Sdl.Event.mouse_button_down then begin
        let mx = Float.of_int (Sdl.Event.get event Sdl.Event.mouse_button_x) in
        let my = Float.of_int (Sdl.Event.get event Sdl.Event.mouse_button_y) in
        handle_click state state.root mx my
      end

      else if typ = Sdl.Event.mouse_wheel then begin
        let wy = Sdl.Event.get event Sdl.Event.mouse_wheel_y in
        state.scroll_y <- state.scroll_y +. (float_of_int wy *. 40.);
        if state.scroll_y > 0. then state.scroll_y <- 0.;
        state.needs_repaint <- true
      end

      else if typ = Sdl.Event.window_event then begin
        let wev = Sdl.Event.get event Sdl.Event.window_event_id in
        if wev = Sdl.Event.window_event_resized
           || wev = Sdl.Event.window_event_size_changed
           || wev = Sdl.Event.window_event_exposed then begin
          let (new_w, new_h) = Sdl.get_window_size window in
          if new_w <> state.win_w || new_h <> state.win_h then begin
            state.win_w <- new_w;
            state.win_h <- new_h;
            (* Recreate texture and surface at new size *)
            Sdl.destroy_texture !texture;
            texture := create_texture new_w new_h;
            Cairo.Surface.finish !surface;
            surface := create_cairo_surface new_w new_h;
            cr := Cairo.create !surface;
            (* Re-layout *)
            Sx_native_layout.measure !cr state.root;
            let w = float_of_int new_w in
            let h = float_of_int new_h -. 36. in
            Sx_native_layout.layout state.root 0. 0. w h
          end;
          state.needs_repaint <- true
        end
      end
    done;

    (* Paint if needed *)
    if state.needs_repaint then begin
      state.needs_repaint <- false;
      let w = float_of_int state.win_w in
      let h = float_of_int state.win_h in

      (* Apply scroll offset to root *)
      state.root.box.y <- state.scroll_y;

      Sx_native_paint.paint_scene !cr state.root state.current_url w h;
      Cairo.Surface.flush !surface;

      (* Restore root position *)
      state.root.box.y <- 0.;

      (* Copy Cairo surface data to SDL texture *)
      let data = Cairo.Image.get_data8 !surface in
      let stride = Bigarray.Array1.dim data / state.win_h in
      (* Lock texture, copy data, unlock *)
      (match Sdl.lock_texture !texture None Bigarray.int8_unsigned with
       | Ok (pixels, _pitch) ->
         let src_len = Bigarray.Array1.dim data in
         let dst_len = Bigarray.Array1.dim pixels in
         let copy_len = min src_len dst_len in
         for i = 0 to copy_len - 1 do
           Bigarray.Array1.set pixels i (Bigarray.Array1.get data i)
         done;
         ignore stride;
         Sdl.unlock_texture !texture
       | Error (`Msg e) ->
         Printf.eprintf "lock_texture error: %s\n%!" e);

      (* Present *)
      ignore (Sdl.render_clear renderer);
      ignore (Sdl.render_copy renderer !texture);
      Sdl.render_present renderer
    end;

    Sdl.delay 16l  (* ~60 fps cap *)
  done;

  (* Cleanup *)
  Sdl.destroy_texture !texture;
  Sdl.destroy_renderer renderer;
  Sdl.destroy_window window
