(* Lightweight thread library for Objective Caml
 * http://www.ocsigen.org/lwt
 * Module Toplevel
 * Copyright (C) 2009 Jérémie Dimino
 *                    Pierre Chambart
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation, with linking exceptions;
 * either version 2.1 of the License, or (at your option) any later
 * version. See COPYING file for details.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA
 * 02111-1307, USA.
 *)

open Types
open Lwt_read_line

module TextSet = Set.Make(Text)

type path =
  | Path of Path.t
  | Longident of Longident.t

module PathMap = Map.Make(struct type t = path let compare = compare end)

let keywords = Lwt_ocaml_completion.keywords

let global_env = ref(lazy(raise Exit))
let local_envs = ref(PathMap.empty)

(* Returns [acc] plus all modules of [dir] *)
let add_modules_from_directory acc dir =
  let dir = if dir = ""  then "./" else dir in
  let acc = ref acc in
  Array.iter (fun fname ->
                if Filename.check_suffix fname ".cmi" then
                  acc := TextSet.add (Text.capitalize (Filename.chop_suffix fname ".cmi")) !acc)
    (Sys.readdir (if dir = "" then Filename.current_dir_name else dir));
  !acc

#if ocaml_version >= (4, 0)

(* List all names of the module with path [path] *)
let get_names_of_module path =
  try
    match
      match path with
        | Path path ->
            Env.find_module path !Toploop.toplevel_env
        | Longident ident ->
            snd (Env.lookup_module ident !Toploop.toplevel_env)
    with
      | Mty_signature decls ->
          List.fold_left
            (fun acc decl -> match decl with
               | Sig_value(id, _)
               | Sig_type(id, _, _)
               | Sig_exception(id, _)
               | Sig_module(id, _, _)
               | Sig_modtype(id, _)
               | Sig_class(id, _, _)
               | Sig_class_type(id, _, _) ->
                   TextSet.add (Ident.name id) acc)
            TextSet.empty decls
      | _ ->
          TextSet.empty
  with Not_found ->
    TextSet.empty

#else

(* List all names of the module with path [path] *)
let get_names_of_module path =
  try
    match
      match path with
        | Path path ->
            Env.find_module path !Toploop.toplevel_env
        | Longident ident ->
            snd (Env.lookup_module ident !Toploop.toplevel_env)
    with
      | Tmty_signature decls ->
          List.fold_left
            (fun acc decl -> match decl with
               | Tsig_value(id, _)
               | Tsig_type(id, _, _)
               | Tsig_exception(id, _)
               | Tsig_module(id, _, _)
               | Tsig_modtype(id, _)
               | Tsig_class(id, _, _)
               | Tsig_cltype(id, _, _) ->
                   TextSet.add (Ident.name id) acc)
            TextSet.empty decls
      | _ ->
          TextSet.empty
  with Not_found ->
    TextSet.empty

#endif

let names_of_module path =
  try
    PathMap.find path !local_envs
  with Not_found ->
    let names = get_names_of_module path in
    local_envs := PathMap.add path names !local_envs;
    names

(* List all names accessible without a path *)
let env_names () =
  let rec loop acc = function
    | Env.Env_empty -> acc
    | Env.Env_value(summary, id, _) -> loop (TextSet.add (Ident.name id) acc) summary
    | Env.Env_type(summary, id, _) -> loop (TextSet.add (Ident.name id) acc) summary
    | Env.Env_exception(summary, id, _) -> loop (TextSet.add (Ident.name id) acc) summary
    | Env.Env_module(summary, id, _) -> loop (TextSet.add (Ident.name id) acc) summary
    | Env.Env_modtype(summary, id, _) -> loop (TextSet.add (Ident.name id) acc) summary
    | Env.Env_class(summary, id, _) -> loop (TextSet.add (Ident.name id) acc) summary
    | Env.Env_cltype(summary, id, _) -> loop (TextSet.add (Ident.name id) acc) summary
    | Env.Env_open(summary, path) -> loop (TextSet.union acc (names_of_module (Path path))) summary
  in
  (* Add names of the environment: *)
  let acc = loop TextSet.empty (Env.summary !Toploop.toplevel_env) in
  (* Add accessible modules: *)
  List.fold_left add_modules_from_directory acc !Config.load_path

let path_of_string text =
  match Text.split ~sep:"." text with
    | [] ->
        invalid_arg "Toplevel.make_path"
    | ident :: rest ->
        let rec loop path = function
          | [] -> Longident path
          | component :: rest -> loop (Longident.Ldot(path, component)) rest
        in
        loop (Longident.Lident ident) rest

let complete_ident before ident after =
  match Text.rev_split ~sep:"." ~max:2 ident with
    | [ident]->
        complete ~suffix:"" before ident after (TextSet.union keywords (Lazy.force !global_env))
    | [path; ident] ->
        let before = before ^ path ^ "." in
        complete ~suffix:"" before ident after (names_of_module (path_of_string path))
    | _ ->
        assert false

let restart () =
  global_env := lazy(env_names ());
  local_envs := PathMap.empty

let () =
  Topfind.don't_load_deeply ["lwt"; "lwt.react"; "lwt.unix"; "lwt.text"; "lwt.top"];
  Lwt_ocaml_completion.complete_ident := complete_ident;
  Lwt_ocaml_completion.restart := restart
