open Tsdl
open Tgl4
open Result

(* the bind operator for the exception monad, for quick error
handling *)
let (>>=) x f = match x with
  | Ok v -> f v
  | Error _ as e -> e

(* our vertex shader takes a 2d position and renders it on the screen,
and that's it *)
let vertex_shader =
  "#version 130
in vec2 position;
void main() {
  gl_Position = vec4(position, 0.0, 1.0);
}
"

(* our fragment shader just draws white *)
let fragment_shader =
  "#version 130
out vec4 out_color;
void main() {
  out_color = vec4(1.0, 1.0, 1.0, 1.0);
}
"

(* we use a lot of Bigarrays, which are contiguous chunks of memory
that we can hand off to C *)
let bigarray_create k len = Bigarray.(Array1.create k c_layout len)

(* our vertex data is [0.0, 0.5, 0.5, -0.5, -0.5, -0.5], but in a
contiguous array of memory *)
let vertices =
  let arr = bigarray_create Bigarray.float32 6 in
  arr.{0} <- 0.0;
  arr.{1} <- 0.5;
  arr.{2} <- 0.5;
  arr.{3} <- -0.5;
  arr.{4} <- -0.5;
  arr.{5} <- -0.5;
  arr

(* This takes a function which expects to fill a buffer with a single
32-bit integer; this will call that function and then conver it to an
OCaml integer *)
let buf_to_int =
  let a = bigarray_create Bigarray.int32 1 in
  fun f -> f a; Int32.to_int a.{0}

(* This takes a function that expects to use a buffer containing a
   single 32-bit integer as well as an integer itself; this will
   convert the integer into a buffer and then call the function on
   it *)
let int_as_buf =
  let a = bigarray_create Bigarray.int32 1 in
  fun f i -> a.{0} <- Int32.of_int i; f a

(* Like buf_to_int, but for an arbitrary-sized string *)
let get_string len f =
  let a = bigarray_create Bigarray.char len in
  f a; Gl.string_of_bigarray a

(* The first thing to do is to get a new window from SDL and then
initialize an OpenGL context in that window. *)
let window () =
  Sdl.init Sdl.Init.video >>= fun () ->
  Sdl.create_window ~w:640 ~h:480 "test" Sdl.Window.opengl >>= fun w ->
  Sdl.gl_create_context w >>= fun ctx ->
  Sdl.gl_make_current w ctx >>= fun () ->
  Ok (w, ctx)

(* This compiles a shader, producing an informative error message if
the compilation process fails. *)
let compile_shader typ src =
  let shader = Gl.create_shader typ in
  Gl.shader_source shader src;
  Gl.compile_shader shader;
  if buf_to_int (Gl.get_shaderiv shader Gl.compile_status) = Gl.true_
  then Ok shader
  else
    let len = buf_to_int (Gl.get_shaderiv shader Gl.info_log_length) in
    let log = get_string len (Gl.get_shader_info_log shader len None) in
    (Gl.delete_shader shader; Error (`Msg log))

(* This links our two shaders together into a single OpenGL program,
again producing an informative error message if it fails. *)
let create_program () =
  compile_shader Gl.vertex_shader vertex_shader >>= fun vertex ->
  compile_shader Gl.fragment_shader fragment_shader >>= fun fragment ->
  let program = Gl.create_program () in
  Gl.attach_shader program vertex;
  Gl.attach_shader program fragment;
  Gl.link_program program;
  if buf_to_int (Gl.get_programiv program Gl.link_status) = Gl.true_
  then Ok program
  else
    let len = buf_to_int (Gl.get_programiv program Gl.info_log_length) in
    let log = get_string len (Gl.get_program_info_log program len None) in
    (Gl.delete_program program; Error (`Msg log))

(* This initializes a vertex array and a vertex buffer using our
vertex information above. *)
let init_scene program =
  let array = buf_to_int (Gl.gen_vertex_arrays 1) in
  Gl.bind_vertex_array array;

  let buffer = buf_to_int (Gl.gen_buffers 1) in
  Gl.bind_buffer Gl.array_buffer buffer;
  Gl.buffer_data
    Gl.array_buffer
    (Gl.bigarray_byte_size vertices)
    (Some vertices)
    Gl.static_draw;

  (* This bit is first telling OpenGL where our fragment data can be
  found *)
  Gl.bind_frag_data_location program 0 "out_color";
  (* and then asking it the location of the position attribute *)
  let pos_attr = Gl.get_attrib_location program "position" in
  (* and finally telling OpenGL that the stuff in our vertex buffer
  corresponds to the aforementioned "position" attribute *)
  Gl.enable_vertex_attrib_array pos_attr;
  (* and tells OpenGL that it consists of two floats with no stride
  and no offset. *)
  Gl.vertex_attrib_pointer pos_attr 2 Gl.float false 0 (`Offset 0);

  (* We return these for later use! *)
  (array, buffer)

let draw program buffer w =
  (* we clear the background color to a pleasing robin's-egg blue *)
  Gl.clear_color 0.3 0.6 0.9 1.;
  Gl.clear Gl.color_buffer_bit;

  (* we tell OpenGL we want to use that program we made earlier, and
  the array buffer we created earlier, too *)
  Gl.use_program program;
  Gl.bind_buffer Gl.array_buffer buffer;
  (* and tell it to draw the first 3 things in that array buffer as
  triangles *)
  Gl.draw_arrays Gl.triangles 0 3;
  (* and then clear that shit out. (We can skip this step, to be fair,
  but it's good to get in the habit for later.) *)
  Gl.bind_buffer Gl.array_buffer 0;
  Gl.use_program 0;

  (* and finally swap the window! *)
  Sdl.gl_swap_window w

(* The drawing function here is of type () -> () so we can pass in a
closure and not have to pass other data down the chain *)
let event_loop draw w =
  let e = Sdl.Event.create() in
  (* these helper functions are just to reduce the verbosity down
  below *)
  let keycode e = Sdl.Scancode.enum Sdl.Event.(get e keyboard_scancode) in
  let event e = Sdl.Event.(enum (get e typ)) in
  (* the crunchy center is a loop that draws and then dispatches on
  the relevant events *)
  let rec loop () =
    draw ();
    Sdl.wait_event (Some e) >>= fun () ->
    match event e with
    | `Quit -> Ok ()
    | `Key_down when keycode e = `Escape -> Ok ()
    | `Key_down when keycode e = `Q -> Ok ()
    | _ -> loop ()
  in loop ()

(* and the main function! *)
let main () =
  (* create the window *)
  window () >>= fun (w, ctx) ->
  (* create the program *)
  create_program () >>= fun program ->
  (* create the vertex data *)
  let (array, buffer) = init_scene program in
  (* run the event loop with the relevant drawing function *)
  event_loop (fun () -> draw program array w) w >>= fun () ->
  (* and clean it all up! *)
  int_as_buf (Gl.delete_vertex_arrays 1) array;
  int_as_buf (Gl.delete_buffers 1) buffer;
  Gl.delete_program program;
  Sdl.gl_delete_context ctx;
  Sdl.destroy_window w;
  Sdl.quit ();
  (* s'all good yo *)
  Ok ()

(* and, at the top-level, run it and print any error messages we
get. *)
let () = match main () with
  | Ok () -> ()
  | Error (`Msg e) ->
     Sdl.log "%s" e;
     exit 1
