module Key = struct
  let sdlkey_map = Hashtbl.create 1024

  let () =
    let aa (x : int) (y : Key.code) = Hashtbl.add sdlkey_map x y in
    aa return `Enter ;
    aa escape `Escape ;
    aa backspace `Backspace ;
    aa tab `Tab ;
    aa f1 (`Function 1) ;
    aa f2 (`Function 2) ;
    aa f3 (`Function 3) ;
    aa f4 (`Function 4) ;
    aa f5 (`Function 5) ;
    aa f6 (`Function 6) ;
    aa f7 (`Function 7) ;
    aa f8 (`Function 8) ;
    aa f9 (`Function 9) ;
    aa f10 (`Function 10) ;
    aa f11 (`Function 11) ;
    aa f12 (`Function 12) ;
    aa insert `Insert ;
    aa delete `Delete ;
    aa home `Home ;
    aa kend `End ;
    aa pageup (`Page `Up) ;
    aa pagedown (`Page `Down) ;
    aa right (`Arrow `Right) ;
    aa left (`Arrow `Left) ;
    aa down (`Arrow `Down) ;
    aa up (`Arrow `Up)

  let key_of_sdlkey ev =
    let (kc : Sdl.keycode) =
      Sdl.Event.get ev Sdl.Event.keyboard_keycode
      land lnot Sdl.K.scancode_mask in
    match (Hashtbl.find_opt sdlkey_map kc, Uchar.is_valid kc) with
    | Some s, _ -> Some s
    | None, true -> Some (`Uchar (Uchar.of_int kc))
    | None, false -> None

  let event_of_sdlevent ev : t option =
    match Sdl.Event.enum (Sdl.Event.get ev Sdl.Event.typ) with
    | (`Key_down | `Key_up) as d -> (
      match key_of_sdlkey ev with
      | None -> None
      | Some code ->
          let km = Sdl.Event.get ev Sdl.Event.keyboard_keymod in
          Some
            (`Key
              ( ( match d with
                | _
                  when Sdl.Event.get ev Sdl.Event.keyboard_repeat > 1
                  ->
                    `Repeat
                | `Key_up -> `Release
                | _ -> `Press )
              , { code
                ; ctrl= km land Sdl.Kmod.ctrl > 0
                ; meta= km land Sdl.Kmod.alt > 0
                ; super= km land Sdl.Kmod.gui > 0
                ; shift= km land Sdl.Kmod.shift > 0 } ) ) )
    | `Mouse_motion ->
        let x, y = snd (Tsdl.Sdl.get_mouse_state ()) in
        Some (`Mouse (V2.v (float x) (float y)))
    | `Quit -> Some `Quit
    (* Unhandled events *)
    | `Text_editing -> Some (`Unknown "`Text_editing")
    | `Text_input -> Some (`Unknown "`Text_input")
    | `App_did_enter_background ->
        Some (`Unknown "`App_did_enter_background")
    | `App_did_enter_foreground ->
        Some (`Unknown "`App_did_enter_foreground ")
    | `App_low_memory -> Some (`Unknown "`App_low_memory ")
    | `App_terminating -> Some (`Unknown "`App_terminating ")
    | `App_will_enter_background ->
        Some (`Unknown "`App_will_enter_background ")
    | `App_will_enter_foreground ->
        Some (`Unknown "`App_will_enter_foreground ")
    | `Clipboard_update -> Some (`Unknown "`Clipboard_update ")
    | `Controller_axis_motion ->
        Some (`Unknown "`Controller_axis_motion ")
    | `Controller_button_down ->
        Some (`Unknown "`Controller_button_down ")
    | `Controller_button_up -> Some (`Unknown "`Controller_button_up ")
    | `Controller_device_added ->
        Some (`Unknown "`Controller_device_added ")
    | `Controller_device_remapped ->
        Some (`Unknown "`Controller_device_remapped ")
    | `Controller_device_removed ->
        Some (`Unknown "`Controller_device_removed ")
    | `Dollar_gesture -> Some (`Unknown "`Dollar_gesture ")
    | `Dollar_record -> Some (`Unknown "`Dollar_record ")
    | `Drop_file -> Some (`Unknown "`Drop_file ")
    | `Finger_down -> Some (`Unknown "`Finger_down")
    | `Finger_motion -> Some (`Unknown "`Finger_motion ")
    | `Finger_up -> Some (`Unknown "`Finger_up ")
    | `Joy_axis_motion -> Some (`Unknown "`Joy_axis_motion ")
    | `Joy_ball_motion -> Some (`Unknown "`Joy_ball_motion ")
    | `Joy_button_down -> Some (`Unknown "`Joy_button_down ")
    | `Joy_button_up -> Some (`Unknown "`Joy_button_up ")
    | `Joy_device_added -> Some (`Unknown "`Joy_device_added ")
    | `Joy_device_removed -> Some (`Unknown "`Joy_device_removed ")
    | `Joy_hat_motion -> Some (`Unknown "`Joy_hat_motion ")
    | `Mouse_button_down -> Some (`Unknown "`Mouse_button_down ")
    | `Mouse_button_up -> Some (`Unknown "`Mouse_button_up")
    | `Mouse_wheel -> Some (`Unknown "`Mouse_wheel ")
    | `Multi_gesture -> Some (`Unknown "`Multi_gesture")
    | `Sys_wm_event -> Some (`Unknown "`Sys_wm_event ")
    | `Unknown e -> Some (`Unknown (Format.sprintf "`Unknown %d" e))
    | `User_event -> Some (`Unknown "`User_event ")
    | `Display_event -> Some (`Unknown "`Display_event ")
    | `Sensor_update -> Some (`Unknown "`Sensor_update ")
    | `Window_event -> Some (`Unknown "`Window_event ")

  let key_up : Sdl.keycode = 0x40000052
  let key_down : Sdl.keycode = 0x40000051
  let key_left : Sdl.keycode = 0x40000050
  let key_right : Sdl.keycode = 0x4000004f
end

module Display = struct
  open Tgles2
  open Tsdl
  open Gg
  open Wall
  module I = Image
  module P = Path

  let ( >>>= ) x f =
    match x with Ok a -> f a | Error _ as result -> result

  let get_result = function
    | Ok x -> x
    | Error (`Msg msg) -> failwith msg

  (* current window state to be passed to window renderer *)
  type state =
    { box: box2
          (* This is cannonically box within which the next element should draw *)
    ; time: float
    ; wall: Wall.renderer }

  (* the box2 here is cannonically the place the returner drew
     (the Wall.image extents) *)
  type image = box2 * Wall.image
  type pane = state -> state * image
  type actor = (Event.events -> pane Lwt.t) ref

  let pane_empty s =
    (s, (Box2.of_pts (Box2.o s.box) (Box2.o s.box), Image.empty))

  type frame =
    { sdl_win: Sdl.window
    ; gl: Sdl.gl_context
    ; wall: Wall.renderer
    ; mutable last_pane: pane
    ; mutable quit: bool
    ; mutable fullscreen: bool }

  let ticks () = Int32.to_float (Sdl.get_ticks ()) /. 1000.

  let on_failure ~cleanup result =
    (match result with Ok _ -> () | Error _ -> cleanup ()) ;
    result

  let video_initialized = lazy (Sdl.init Sdl.Init.video)

  let make_frame ?(title = "komm") ~w ~h () =
    Lazy.force video_initialized
    >>>= fun () ->
    Sdl.create_window ~w ~h title
      Sdl.Window.(
        opengl + allow_highdpi + resizable (*+ input_grabbed*))
    >>>= fun sdl_win ->
    Sdl.set_window_title sdl_win title ;
    ignore (Sdl.gl_set_swap_interval (-1)) ;
    ignore (Sdl.gl_set_attribute Sdl.Gl.stencil_size 1) ;
    on_failure
      ( Sdl.gl_create_context sdl_win
      >>>= fun gl ->
      let wall =
        Wall.Renderer.create ~antialias:true ~stencil_strokes:true ()
      in
      Ok
        { sdl_win
        ; gl
        ; wall
        ; quit= false
        ; fullscreen= false
        ; last_pane= pane_empty } )
      ~cleanup:(fun () -> Sdl.destroy_window sdl_win)

  let handle_frame_events frame events =
    List.iter
      (fun (e : Event.t) ->
        match e with
        | `Quit -> frame.quit <- true
        | `Fullscreen a ->
            frame.fullscreen <- a ;
            ignore (Sdl.show_cursor (not frame.fullscreen) : _ result) ;
            ignore
              ( Sdl.set_window_fullscreen frame.sdl_win
                  ( if frame.fullscreen then
                    Sdl.Window.fullscreen_desktop
                  else Sdl.Window.windowed )
                : _ result )
        | _ -> () )
      events

  let draw_pane frame pane =
    let width, height = Sdl.gl_get_drawable_size frame.sdl_win in
    let _, (_, image) =
      pane
        { box= Box2.v (P2.v 0. 0.) (P2.v (float width) (float height))
        ; time= ticks ()
        ; wall= frame.wall } in
    Sdl.gl_make_current frame.sdl_win frame.gl
    >>>= fun () ->
    Gl.viewport 0 0 width height ;
    Gl.clear_color 0.0 0.0 0.0 1.0 ;
    Gl.(
      clear
        (color_buffer_bit lor depth_buffer_bit lor stencil_buffer_bit)) ;
    Gl.enable Gl.blend ;
    Gl.blend_func_separate Gl.one Gl.src_alpha Gl.one
      Gl.one_minus_src_alpha ;
    Gl.enable Gl.cull_face_enum ;
    Gl.disable Gl.depth_test ;
    let width = float width and height = float height in
    Wall.Renderer.render frame.wall ~width ~height image ;
    Sdl.gl_swap_window frame.sdl_win ;
    Ok ()

  let rec get_events () : Event.t list =
    (* create and fill event list *)
    let ev = Sdl.Event.create () in
    if Sdl.poll_event (Some ev) then
      match Event.event_of_sdlevent ev with
      | Some e -> get_events () @ [e]
      | None -> get_events ()
    else []

  let successful_actor = ref (fun _ -> Lwt.return pane_empty)

  let display_frame frame (actor : actor) =
    let events = get_events () in
    handle_frame_events frame events ;
    if List.length events > 0 then (
      (* recompute the actor definition with the new events to return a new pane *)
      ( try
          !actor events
          >|= fun p ->
          successful_actor := !actor ;
          p
        with e ->
          F.epr
            "Display.display_frame (!actor events) failed with:@. %s \
             @."
            (Printexc.to_string e) ;
          actor := !successful_actor ;
          !actor events )
      >>= fun p ->
      frame.last_pane <- p ;
      (* call draw_pane because we should redraw now that we have updated *)
      ignore (draw_pane frame frame.last_pane) ;
      Lwt.return_unit )
    else Lwt.return_unit

  let run frame actor () =
    let frame = get_result frame in
    Sdl.show_window frame.sdl_win ;
    let rec loop () =
      Lwt.pause () (* seems required for the irc connection to work *)
      >>= fun () ->
      Lwt_unix.sleep 0.030
      >>= fun () ->
      display_frame frame actor
      >>= fun () ->
      if not frame.quit then loop () else Lwt.return_unit in
    Lwt_main.run (loop ()) ;
    print_endline "quit" ;
    Sdl.hide_window frame.sdl_win ;
    Sdl.gl_delete_context frame.gl ;
    Sdl.destroy_window frame.sdl_win ;
    Sdl.quit () ;
    ()

  let gray ?(a = 1.0) v = Color.v v v v a

  module FontCache = Map.Make (String)

  let font_cache = ref FontCache.empty

  let load_font name =
    match FontCache.find_opt name !font_cache with
    | Some font -> font
    | None -> (
        let ic = open_in_bin name in
        let dim = in_channel_length ic in
        let fd = Unix.descr_of_in_channel ic in
        let buffer =
          Unix.map_file fd Bigarray.int8_unsigned Bigarray.c_layout
            false [|dim|]
          |> Bigarray.array1_of_genarray in
        let offset = List.hd (Stb_truetype.enum buffer) in
        match Stb_truetype.init buffer offset with
        | None -> assert false
        | Some font ->
            font_cache := FontCache.add name font !font_cache ;
            font )

  let font_icons = lazy (load_font "fonts/entypo.ttf")
  let font_sans = lazy (load_font "fonts/Roboto-Regular.ttf")
  let font_sans_bold = lazy (load_font "fonts/Roboto-Bold.ttf")
  let font_sans_light = lazy (load_font "fonts/Roboto-Light.ttf")
  let font_sans_italic = lazy (load_font "fonts/Roboto-Italic.ttf")

  let font_sans_bold_italic =
    lazy (load_font "fonts/Roboto-BoldItalic.ttf")

  let font_serif =
    lazy (load_font "fonts/ScheherazadeNew-Regular.ttf")

  let font_serif_bold =
    lazy (load_font "fonts/ScheherazadeNew-Bold.ttf")

  let font_mono = lazy (load_font "fonts/static/RobotoMono-Regular")

  let font_mono_bold =
    lazy (load_font "fonts/static/RobotoMono-Regular")

  let font_mono_light =
    lazy (load_font "fonts/static/RobotoMono-Regular")

  let font_emoji = lazy (load_font "fonts/NotoEmoji-Regular.ttf")

  let str_of_box b =
    Printf.sprintf "(ox:%0.1f oy:%0.1f ex%0.1f ey%0.1f)" (Box2.ox b)
      (Box2.oy b) (Box2.maxx b) (Box2.maxy b)

  let draw_label text b =
    let f =
      Wall_text.Font.make ~size:(Box2.h b) (Lazy.force font_sans)
    in
    ( Box2.v (Box2.o b)
        (P2.v (Wall_text.Font.text_width f text) (Box2.h b))
    , I.paint
        (Paint.color (gray ~a:0.5 1.0))
        Wall_text.(
          simple_text f ~valign:`BASELINE ~halign:`LEFT ~x:(Box2.ox b)
            ~y:(Box2.oy b +. (Box2.h b *. 0.75))
            text) )

  let fill_box c b =
    ( b
    , I.paint (Paint.color c)
        ( I.fill_path
        @@ fun t ->
        P.rect t ~x:(Box2.ox b) ~y:(Box2.oy b) ~w:(Box2.w b)
          ~h:(Box2.h b) ) )

  let draw_filled_box c (s : state) = (s, fill_box c s.box)

  let path_box c b (s : state) =
    ( s
    , ( b
      , I.paint (Paint.color c)
          ( I.stroke_path (Outline.make ())
          @@ fun t ->
          P.rect t ~x:(Box2.ox b) ~y:(Box2.oy b) ~w:(Box2.w b)
            ~h:(Box2.h b) ) ) )

  let path_circle c b (s : state) =
    ( s
    , ( b
      , I.paint (Paint.color c)
          ( I.stroke_path (Outline.make ())
          @@ fun t ->
          P.circle t ~cx:(Box2.midx b) ~cy:(Box2.midy b)
            ~r:(Box2.w b /. 2.) ) ) )

  (** Display.state.box as supplied to a widget defines the allowed drawing area for the widget.
    This way basic widgets will just expand to the full area of a box, while other widgets can have
    the express purpose of limiting the size of an object in a larger system of limitations. 

    Panes return a tuple: (state, (box, image))
    state is the updated state, where state.box is always 
      - the top left corner of the box the pane drew in, and
      - the bottom right corner of the state.box that was passed in
    box is the area the widget actually drew in (or wants to sort of "use")
    image is the Wall.image to compose with other panes and draw to the display
  *)

  let simple_text f text (s : state) =
    let fm = Wall_text.Font.font_metrics f in
    let font_height = fm.ascent -. fm.descent +. fm.line_gap in
    let tm = Wall_text.Font.text_measure f text in
    let br_pt =
      P2.v (Box2.ox s.box +. tm.width) (Box2.oy s.box +. font_height)
    in
    let bextent = Box2.of_pts (Box2.o s.box) br_pt in
    (*  let _, (_, redbox) = path_box Color.red bextent s in*)
    ( {s with box= Box2.of_pts (Box2.br_pt bextent) (Box2.max s.box)}
    , ( bextent
      , (* I.stack redbox *)
        I.paint
          (Paint.color (gray ~a:0.5 1.0))
          Wall_text.(
            simple_text f ~valign:`BASELINE ~halign:`LEFT
              ~x:(Box2.ox s.box)
              ~y:(Box2.oy s.box +. fm.ascent)
              text) ) )

  let pane_box next_point_func (subpanes : pane list) (so : state) =
    let sr, (br, ir) =
      List.fold_left
        (fun (sp, (bp, ip)) (pane : pane) ->
          (* uses br to hold max extent of boxes *)
          let sr, (br, ir) = pane sp in
          (* draw the pane *)
          let _, (_, irb) = path_box Color.blue br sr in
          (* draw the box around the pane *)
          ( { sr with
              box= Box2.of_pts (next_point_func br) (Box2.max sp.box)
            }
          , ( Box2.of_pts (Box2.o bp)
                (P2.v
                   (max (Box2.maxx br) (Box2.maxx bp))
                   (max (Box2.maxy br) (Box2.maxy bp)) )
            , Image.seq [ip; irb; ir] ) ) )
        ( so
        , (Box2.of_pts (Box2.o so.box) (Box2.o so.box), Image.empty)
        )
        subpanes in
    let _, (_, redbox) = path_box Color.red br sr in
    (sr, (br, Image.stack redbox ir))
end
