open Import

module File = struct
  type t =
    { path : string
    ; contents : string
    }
end

let _pid_of_path path =
  Filename.basename path
  |> Filename.chop_suffix_opt ~suffix:".pid"
  |> Option.bind ~f:Int.of_string
;;

module Dune = struct
  module T = struct
    type t =
      { root : string
      ; pid : int
      ; where : Where.t
      }

    let compare t { root; pid; where } =
      let open Ordering.O in
      let= () = Int.compare t.pid pid in
      let= () = String.compare t.root root in
      Where.compare t.where where
    ;;

    let to_dyn { root; pid; where } =
      let open Dyn in
      record [ "root", string root; "pid", int pid; "where", Where.to_dyn where ]
    ;;
  end

  include T
  module C = Comparable.Make (T)
  module Set = C.Set

  let create ~where ~root ~pid = { where; root; pid }
  let root t = t.root
  let where t = t.where
  let pid t = t.pid
  let filename dune = Printf.sprintf "%d.csexp" dune.pid

  let sexp : t Conv.value =
    let open Conv in
    let to_ (where, root, pid) = { where; root; pid } in
    let from { where; root; pid } = where, root, pid in
    let where = field "where" (required Where.sexp) in
    let root = field "root" (required string) in
    let pid = field "pid" (required int) in
    iso (record (three where root pid)) to_ from
  ;;

  type error =
    | Of_sexp of Conv.error
    | Csexp of
        { position : int
        ; message : string
        }

  exception E of error

  let () =
    Printexc.register_printer (function
      | E (Of_sexp e) -> Some (Dyn.to_string (Conv.dyn_of_error e))
      | E (Csexp { position; message }) ->
        Some
          (Dyn.to_string
             (let open Dyn in
              record [ "message", string message; "position", int position ]))
      | _ -> None)
  ;;

  let of_file (f : File.t) =
    match Csexp.parse_string f.contents with
    | Error (position, message) -> Error (Csexp { position; message })
    | Ok s ->
      (match Conv.of_sexp sexp ~version:(0, 0) s with
       | Ok s -> Ok s
       | Error e -> Error (Of_sexp e))
  ;;
end

module Config = struct
  type t = Xdg.t

  let watch_dir t =
    let dir =
      match Xdg.runtime_dir t with
      | Some runtime -> runtime
      | None -> Xdg.data_dir t
    in
    Filename.concat dir "dune/rpc"
  ;;

  let create x = x

  let register t dune =
    let file =
      let contents = Conv.to_sexp Dune.sexp dune |> Csexp.to_string in
      let path = Filename.concat (watch_dir t) (Dune.filename dune) in
      { File.contents; path }
    in
    `Caller_should_write file
  ;;
end

type nonrec t =
  { config : Config.t
  ; mutable last_mtime : float option
  ; mutable current : Dune.t list
  }

let create config = { config; last_mtime = None; current = [] }
let current t = t.current

module Refresh = struct
  type t =
    { added : Dune.t list
    ; removed : Dune.t list
    ; errored : (string * exn) list
    }

  let empty = { added = []; removed = []; errored = [] }
  let added t = t.added
  let removed t = t.removed
  let errored t = t.errored
end

module Poll
    (Fiber : sig
       type 'a t

       val return : 'a -> 'a t
       val parallel_map : 'a list -> f:('a -> 'b t) -> 'b list t

       module O : sig
         val ( let* ) : 'a t -> ('a -> 'b t) -> 'b t
         val ( let+ ) : 'a t -> ('a -> 'b) -> 'b t
       end
     end)
    (IO : sig
       val scandir : string -> (string list, exn) result Fiber.t
       val stat : string -> ([ `Mtime of float ], exn) result Fiber.t
       val read_file : string -> (string, exn) result Fiber.t
     end) =
struct
  open Fiber.O

  let ( let** ) x f =
    let* x = x in
    match x with
    | Ok s -> f s
    | Error e -> Fiber.return (Error e)
  ;;

  let ( let++ ) x f =
    let+ x = x in
    match x with
    | Ok s -> Ok (f s)
    | Error e -> Error e
  ;;

  let poll t =
    let dir = Config.watch_dir t.config in
    let** (`Mtime mtime) = IO.stat dir in
    let skip =
      match t.last_mtime with
      | Some last_mtime -> last_mtime <> mtime
      | None ->
        t.last_mtime <- Some mtime;
        false
    in
    if skip
    then Fiber.return (Ok Refresh.empty)
    else
      let++ results =
        let** contents = IO.scandir dir in
        let contents =
          List.filter contents ~f:(fun fname -> fname <> "." && fname <> "..")
        in
        let+ res =
          Fiber.parallel_map contents ~f:(fun fname ->
            let path = Filename.concat dir fname in
            let+ contents = IO.read_file path in
            ( path
            , match contents with
              | Error e -> Error e
              | Ok contents ->
                let file = { File.contents; path } in
                (match Dune.of_file file with
                 | Ok _ as s -> s
                 | Error e -> Error (Dune.E e)) ))
        in
        Ok res
      in
      let new_current, errored =
        List.partition_map results ~f:(fun (file, res) ->
          match res with
          | Ok s -> Left s
          | Error e -> Right (file, e))
      in
      let current = t.current in
      t.current <- new_current;
      let module Set = Dune.Set in
      let new_current = Set.of_list new_current in
      let current = Set.of_list current in
      { Refresh.added = Set.to_list (Set.diff new_current current)
      ; removed = Set.to_list (Set.diff current new_current)
      ; errored
      }
  ;;
end
