(* Merlin representation of a textual source code *)
open Std

let { Logger.log } = Logger.for_section "Msource"

type t = { text : string }

module Digest = struct
  type t = Digest.t

  let make { text } = Digest.string text
  let equal = Digest.equal
end

let dump t = `Assoc [ ("text", `String t.text) ]

let print_position () = function
  | `Start -> "start"
  | `Offset o -> string_of_int o
  | `Logical (l, c) -> string_of_int l ^ ":" ^ string_of_int c
  | `End -> "end"

let make text = { text }

(* Position management *)

type position = [ `Start | `Offset of int | `Logical of int * int | `End ]

let equal_position p1 p2 =
  match (p1, p2) with
  | `Start, `Start | `End, `End -> true
  | `Offset o1, `Offset o2 -> Int.equal o1 o2
  | `Logical (l1, c1), `Logical (l2, c2) -> Int.equal l1 l2 && Int.equal c1 c2
  | _ -> false

exception Found of int

let find_line line { text } =
  if line <= 0 then
    Printf.ksprintf invalid_arg
      "Msource.find_line: invalid line number %d. Numbering starts from 1" line;
  if line = 1 then 0
  else
    let line' = ref line in
    try
      for i = 0 to String.length text - 1 do
        if text.[i] = '\n' then begin
          decr line';
          if !line' = 1 then raise (Found i)
        end
      done;
      log ~title:"find_line" "line %d out of bounds (max = %d)" line
        (line - !line');
      String.length text
    with Found n -> n + 1

let find_offset ({ text } as t) line col =
  assert (col >= 0);
  let offset = find_line line t in
  if col = 0 then offset
  else
    try
      for i = offset to min (offset + col) (String.length text) - 1 do
        if text.[i] = '\n' then begin
          log ~title:"find_offset"
            "%d:%d out of line bounds, line %d only has %d columns" line col
            line (i - offset);
          raise (Found i)
        end
      done;
      if offset + col > String.length text then begin
        log ~title:"find_offset" "%d:%d out of file bounds" line col
      end;
      offset + col
    with Found off -> off

let get_offset t = function
  | `Start -> `Offset 0
  | `Offset x ->
    assert (x >= 0);
    if x <= String.length t.text then `Offset x
    else begin
      log ~title:"get_offset" "offset %d out of bounds (size is %d)" x
        (String.length t.text);
      `Offset (String.length t.text)
    end
  | `End -> `Offset (String.length t.text)
  | `Logical (line, col) -> `Offset (find_offset t line col)

let get_logical { text } = function
  | `Start -> `Logical (1, 0)
  | `Logical _ as p -> p
  | (`Offset _ | `End) as r ->
    let len = String.length text in
    let offset =
      match r with
      | `Offset x when x > len ->
        log ~title:"get_logical" "offset %d out of bounds (size is %d)" x len;
        len
      | `Offset x ->
        assert (x >= 0);
        x
      | `End -> len
    in
    let line = ref 1 in
    let cnum = ref 0 in
    for i = 0 to offset - 1 do
      if text.[i] = '\n' then begin
        incr line;
        cnum := i + 1
      end
    done;
    `Logical (!line, offset - !cnum)

let compare_position t x y =
  let (`Offset ox) = get_offset t x in
  let (`Offset oy) = get_offset t y in
  compare ox oy

let get_lexing_pos t ~filename pos =
  let (`Offset o) = get_offset t pos in
  let (`Logical (line, col)) = get_logical t pos in
  { Lexing.pos_fname = filename;
    pos_lnum = line;
    pos_bol = o - col;
    pos_cnum = o
  }

let substitute t starting ending text =
  let len = String.length t.text in
  let (`Offset starting) = get_offset t starting in
  let (`Offset ending) =
    match ending with
    | `End -> `Offset len
    | `Length l ->
      if starting + l <= len then `Offset (starting + l)
      else begin
        log ~title:"substitute"
          "offset %d + length %d out of bounds (size is %d)" starting l len;
        `Offset len
      end
    | #position as p -> get_offset t p
  in
  if ending < starting then invalid_arg "Source.substitute: ending < starting";
  let text =
    String.sub t.text ~pos:0 ~len:starting
    ^ text
    ^ String.sub t.text ~pos:ending ~len:(len - ending)
  in
  { text }

(* Accessing content *)

let text t = t.text
