/*
 * vim: set ft=rust:
 * vim: set ft=reason:
 *
 * Copyright (c) 2009 - 2013 Monoidics ltd.
 * Copyright (c) 2013 - present Facebook, Inc.
 * All rights reserved.
 *
 * This source code is licensed under the BSD style license found in the
 * LICENSE file in the root directory of this source tree. An additional grant
 * of patent rights can be found in the PATENTS file in the same directory.
 */

open! Utils;


/** Module for Names and Identifiers */
let module L = Logging;

let module F = Format;

type name = string;

type fieldname = {fpos: int, fname: Mangled.t};

type kind = int;

let kprimed = (-1);

let knormal = 0;

let kfootprint = 1;


/** special kind of "null ident" (basically, a more compact way of implementing an ident option).
    useful for situations when an instruction requires an id, but no one should read the result. */
let knone = 2;

/* timestamp for a path identifier */
let path_ident_stamp = (-3);

type t = {kind: int, name: name, stamp: int};

type _ident = t;


/** {2 Comparison Functions} */
let name_compare = string_compare;

let fieldname_compare fn1 fn2 => {
  let n = int_compare fn1.fpos fn2.fpos;
  if (n != 0) {
    n
  } else {
    Mangled.compare fn1.fname fn2.fname
  }
};

let name_equal = string_equal;

let kind_equal k1 k2 => k1 === k2;

let compare i1 i2 => {
  let n = i2.kind - i1.kind;
  if (n != 0) {
    n
  } else {
    let n = name_compare i1.name i2.name;
    if (n != 0) {
      n
    } else {
      int_compare i1.stamp i2.stamp
    }
  }
};

let equal i1 i2 => i1.stamp === i2.stamp && i1.kind === i2.kind && name_equal i1.name i2.name
/* most unlikely first */;

let fieldname_equal fn1 fn2 => fieldname_compare fn1 fn2 == 0;

let rec ident_list_compare il1 il2 =>
  switch (il1, il2) {
  | ([], []) => 0
  | ([], _) => (-1)
  | (_, []) => 1
  | ([i1, ...l1], [i2, ...l2]) =>
    let n = compare i1 i2;
    if (n != 0) {
      n
    } else {
      ident_list_compare l1 l2
    }
  };

let ident_list_equal ids1 ids2 => ident_list_compare ids1 ids2 == 0;


/** {2 Set for identifiers} */
let module IdentSet = Set.Make {
  type t = _ident;
  let compare = compare;
};

let module IdentMap = Map.Make {
  type t = _ident;
  let compare = compare;
};

let module IdentHash = Hashtbl.Make {
  type t = _ident;
  let equal = equal;
  let hash (id: t) => Hashtbl.hash id;
};

let module FieldSet = Set.Make {
  type t = fieldname;
  let compare = fieldname_compare;
};

let module FieldMap = Map.Make {
  type t = fieldname;
  let compare = fieldname_compare;
};

let idlist_to_idset ids => IList.fold_left (fun set id => IdentSet.add id set) IdentSet.empty ids;


/** {2 Conversion between Names and Strings} */
let module StringHash = Hashtbl.Make {
  type t = string;
  let equal (s1: string) (s2: string) => s1 == s2;
  let hash = Hashtbl.hash;
};

let module NameHash = Hashtbl.Make {
  type t = name;
  let equal = name_equal;
  let hash = Hashtbl.hash;
};


/** Convert a string to a name */
let string_to_name (s: string) => s;


/** Create a field name with the given position (field number in the CSU) */
let create_fieldname (n: Mangled.t) (position: int) => {fpos: position, fname: n};


/** Convert a name to a string. */
let name_to_string (name: name) => name;


/** Convert a fieldname to a string. */
let fieldname_to_string fn => Mangled.to_string fn.fname;


/** Convert a fieldname to a string, including the mangled part. */
let fieldname_to_complete_string fn => Mangled.to_string_full fn.fname;


/** Convert a fieldname to a simplified string with at most one-level path. */
let fieldname_to_simplified_string fn => {
  let s = Mangled.to_string fn.fname;
  switch (string_split_character s '.') {
  | (Some s1, s2) =>
    switch (string_split_character s1 '.') {
    | (Some _, s4) => s4 ^ "." ^ s2
    | _ => s
    }
  | _ => s
  }
};


/** Convert a fieldname to a flat string without path. */
let fieldname_to_flat_string fn => {
  let s = Mangled.to_string fn.fname;
  switch (string_split_character s '.') {
  | (Some _, s2) => s2
  | _ => s
  }
};


/** Returns the class part of the fieldname */
let java_fieldname_get_class fn => {
  let fn = fieldname_to_string fn;
  let ri = String.rindex fn '.';
  String.sub fn 0 ri
};


/** Returns the last component of the fieldname */
let java_fieldname_get_field fn => {
  let fn = fieldname_to_string fn;
  let ri = 1 + String.rindex fn '.';
  String.sub fn ri (String.length fn - ri)
};


/** Check if the field is the synthetic this$n of a nested class, used to access the n-th outher instance. */
let java_fieldname_is_outer_instance fn => {
  let fn = fieldname_to_string fn;
  let fn_len = String.length fn;
  let this = ".this$";
  let this_len = String.length this;
  let zero_to_nine s => s >= "0" && s <= "9";
  fn_len > this_len &&
    String.sub fn (fn_len - this_len - 1) this_len == this &&
    zero_to_nine (String.sub fn (fn_len - 1) 1)
};

let fieldname_offset fn => fn.fpos;


/** hidded fieldname constant */
let fieldname_hidden = create_fieldname (Mangled.from_string ".hidden") 0;


/** hidded fieldname constant */
let fieldname_is_hidden fn => fieldname_equal fn fieldname_hidden;


/** {2 Functions and Hash Tables for Managing Stamps} */
/** Set the stamp of the identifier */
let set_stamp i stamp => {...i, stamp};


/** Get the stamp of the identifier */
let get_stamp i => i.stamp;

let module NameGenerator = {
  type t = NameHash.t int;
  let create () :t => NameHash.create 17;

  /** Map from names to stamps. */
  let name_map = ref (create ());
  let get_current () => !name_map;
  let set_current map => name_map := map;

  /** Reset the name generator */
  let reset () => name_map := create ();

  /** Create a fresh identifier with the given kind and name. */
  let create_fresh_ident kind name => {
    let stamp =
      try {
        let stamp = NameHash.find !name_map name;
        NameHash.replace !name_map name (stamp + 1);
        stamp + 1
      } {
      | Not_found =>
        NameHash.add !name_map name 0;
        0
      };
    {kind, name, stamp}
  };

  /** Make sure that fresh ids after whis one will be with different stamps */
  let update_name_hash name stamp =>
    try {
      let curr_stamp = NameHash.find !name_map name;
      let new_stamp = max curr_stamp stamp;
      NameHash.replace !name_map name new_stamp
    } {
    | Not_found => NameHash.add !name_map name stamp
    };
};


/** Name used for primed tmp variables */
let name_primed = string_to_name "t";


/** Name used for normal tmp variables */
let name_normal = string_to_name "n";


/** Name used for footprint tmp variables */
let name_footprint = string_to_name "f";


/** Name used for spec variables */
let name_spec = string_to_name "val";


/** Name used for the return variable */
let name_return = Mangled.from_string "return";


/** Return the standard name for the given kind */
let standard_name kind =>
  if (kind === knormal || kind === knone) {
    name_normal
  } else if (kind === kfootprint) {
    name_footprint
  } else {
    name_primed
  };


/** Every identifier with a given stamp should unltimately be created using this function */
let create_with_stamp kind name stamp => {
  NameGenerator.update_name_hash name stamp;
  {kind, name, stamp}
};


/** Create an identifier with default name for the given kind */
let create kind stamp => create_with_stamp kind (standard_name kind) stamp;


/** Generate a normal identifier with the given name and stamp */
let create_normal name stamp => create_with_stamp knormal name stamp;


/** Create a fresh identifier with default name for the given kind. */
let create_fresh kind => NameGenerator.create_fresh_ident kind (standard_name kind);

let create_none () => create_fresh knone;


/** Generate a primed identifier with the given name and stamp */
let create_primed name stamp => create_with_stamp kprimed name stamp;


/** Generate a footprint identifier with the given name and stamp */
let create_footprint name stamp => create_with_stamp kfootprint name stamp;


/** {2 Functions for Identifiers} */
/** Get a name of an identifier */
let get_name id => id.name;

let is_primed (id: t) => id.kind === kprimed;

let is_normal (id: t) => id.kind === knormal || id.kind === knone;

let is_footprint (id: t) => id.kind === kfootprint;

let is_none (id: t) => id.kind == knone;

let is_path (id: t) => id.kind === knormal && id.stamp == path_ident_stamp;

let make_unprimed id =>
  if (id.kind != kprimed) {
    assert false
  } else if (id.kind === knone) {
    {...id, kind: knone}
  } else {
    {...id, kind: knormal}
  };


/** Update the name generator so that the given id's are not generated again */
let update_name_generator ids => {
  let upd id => ignore (create_with_stamp id.kind id.name id.stamp);
  IList.iter upd ids
};


/** Generate a normal identifier whose name encodes a path given as a string. */
let create_path pathstring =>
  create_normal (string_to_name ("%path%" ^ pathstring)) path_ident_stamp;


/** {2 Pretty Printing} */
/** Convert an identifier to a string. */
let to_string id =>
  if (id.kind === knone) {
    "_"
  } else {
    let base_name = name_to_string id.name;
    let prefix =
      if (id.kind === kfootprint) {
        "@"
      } else if (id.kind === knormal) {
        ""
      } else {
        "_"
      };
    let suffix = "$" ^ string_of_int id.stamp;
    prefix ^ base_name ^ suffix
  };


/** Pretty print a name. */
let pp_name f name => F.fprintf f "%s" (name_to_string name);

let pp_fieldname f fn =>
  /* only use for debug F.fprintf f "%a#%d" pp_name fn.fname fn.fpos */
  Mangled.pp f fn.fname;


/** Pretty print a name in latex. */
let pp_name_latex style f (name: name) => Latex.pp_string style f (name_to_string name);

let pp_fieldname_latex style f fn => Latex.pp_string style f (Mangled.to_string fn.fname);


/** Pretty print an identifier. */
let pp pe f id =>
  switch pe.pe_kind {
  | PP_TEXT
  | PP_HTML => F.fprintf f "%s" (to_string id)
  | PP_LATEX =>
    let base_name = name_to_string id.name;
    let style =
      if (id.kind == kfootprint) {
        Latex.Boldface
      } else if (id.kind == knormal) {
        Latex.Roman
      } else {
        Latex.Roman
      };
    F.fprintf f "%a_{%s}" (Latex.pp_string style) base_name (string_of_int id.stamp)
  };


/** pretty printer for lists of identifiers */
let pp_list pe => pp_comma_seq (pp pe);


/** pretty printer for lists of names */
let pp_name_list = pp_comma_seq pp_name;
