%% -*- erlang-indent-level: 2 -*-
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%%     http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.

%%%-------------------------------------------------------------------
%%% File    : dialyzer_codeserver.erl
%%% Author  : Tobias Lindahl <tobiasl@it.uu.se>
%%% Description :
%%%
%%% Created :  4 Apr 2005 by Tobias Lindahl <tobiasl@it.uu.se>
%%%-------------------------------------------------------------------
-module(dialyzer_codeserver).
-moduledoc false.

-export([delete/1,
         store_temp_contracts/4,
         give_away/2,
         finalize_contracts/1,
         finalize_exported_types/2,
         finalize_records/1,
         get_contracts/1,
         get_callbacks/1,
         get_exported_types_table/1,
         extract_exported_types/1,
         get_exports/1,
         get_records_table/1,
         extract_records/1,
         get_next_core_label/1,
         get_temp_contracts/2,
         all_temp_modules/1,
         store_contracts/4,
         get_temp_exported_types/1,
         get_temp_records_table/1,
         lookup_temp_mod_records/2,
         insert/3,
         insert_exports/2,
         insert_temp_exported_types/2,
         insert_fun_meta_info/2,
         is_exported/2,
         is_member_meta_info/2,
         lookup_mod_code/2,
         lookup_mfa_code/2,
         lookup_mfa_var_label/2,
         lookup_mod_records/2,
         lookup_mod_contracts/2,
         lookup_mfa_contract/2,
         lookup_meta_info/2,
         new/0,
         set_next_core_label/2,
         store_temp_records/3,
         translate_fake_file/3]).

-export_type([codeserver/0, fun_meta_info/0, contracts/0]).

-include("dialyzer.hrl").

%%--------------------------------------------------------------------

-type dict_ets() :: ets:tid().
-type map_ets()  :: ets:tid().
-type  set_ets() :: ets:tid().

-type types()         :: erl_types:type_table().

-type contracts()     :: #{mfa() => dialyzer_contracts:file_contract()}.

%% A property-list of data compiled from -compile and -dialyzer attributes.
-type meta_info()     :: [{{'nowarn_function' | dial_warn_tag()},
                           'mod' | 'func'}].
-type fun_meta_info() :: [{mfa(), meta_info()}
                          | {module(), [dial_warn_tag()]}].

-record(codeserver, {next_core_label = 0 :: label(),
		     code		 :: dict_ets(),
                     exported_types      :: 'clean' | set_ets(), % set(mfa())
		     records             :: 'clean' | map_ets(),
		     contracts           :: map_ets(),
		     callbacks           :: map_ets(),
                     fun_meta_info       :: dict_ets(), % {mfa(), meta_info()}
		     exports             :: 'clean' | set_ets(), % set(mfa())
                     temp_exported_types :: 'clean' | set_ets(), % set(mfa())
		     temp_records        :: 'clean' | map_ets(),
		     temp_contracts      :: 'clean' | map_ets(),
		     temp_callbacks      :: 'clean' | map_ets()
		    }).

-opaque codeserver() :: #codeserver{}.

%%--------------------------------------------------------------------

%% We KNOW that `error` is not a valid value in the table.
ets_dict_find(Key, Table) ->
  case ets:lookup_element(Table, Key, 2, error) of
    error -> error;
    Val -> {ok, Val}
  end.

ets_map_store(Key, Element, Table) ->
  true = ets:insert(Table, {Key, Element}),
  Table.

ets_dict_to_dict(Table) ->
  Fold = fun({Key,Value}, Dict) -> dict:store(Key, Value, Dict) end,
  ets:foldl(Fold, dict:new(), Table).

ets_set_is_element(Key, Table) ->
  ets:member(Table, Key).

ets_set_insert_set(Set, Table) ->
  ets_set_insert_list(sets:to_list(Set), Table).

ets_set_insert_list(List, Table) ->
  true = ets:insert(Table, [{E} || E <- List]).

ets_set_to_set(Table) ->
  Fold = fun({E}, Set) -> sets:add_element(E, Set) end,
  ets:foldl(Fold, sets:new([{version, 2}]), Table).

%%--------------------------------------------------------------------

-spec new() -> codeserver().

new() ->
  CodeOptions = [compressed, public, {read_concurrency, true}],
  Code = ets:new(dialyzer_codeserver_code, CodeOptions),
  ReadOptions = [compressed, {read_concurrency, true}],
  [Records, ExportedTypes] =
    [ets:new(Name, ReadOptions) ||
      Name <- [dialyzer_codeserver_records,
               dialyzer_codeserver_exported_types]],
  ReadWriteOptions = [public | ReadOptions],
  [Contracts, Callbacks] =
    [ets:new(Name, ReadWriteOptions) ||
      Name <- [dialyzer_codeserver_contracts,
               dialyzer_codeserver_callbacks]],
  TempOptions = [public, {write_concurrency, true}],
  [Exports, FunMetaInfo, TempExportedTypes, TempRecords, TempContracts,
   TempCallbacks] =
    [ets:new(Name, TempOptions) ||
      Name <-
	[dialyzer_codeserver_exports, dialyzer_codeserver_fun_meta_info,
         dialyzer_codeserver_temp_exported_types,
	 dialyzer_codeserver_temp_records, dialyzer_codeserver_temp_contracts,
	 dialyzer_codeserver_temp_callbacks]],
  #codeserver{code                = Code,
	      exports             = Exports,
              fun_meta_info       = FunMetaInfo,
              exported_types      = ExportedTypes,
              records             = Records,
              contracts           = Contracts,
              callbacks           = Callbacks,
	      temp_exported_types = TempExportedTypes,
	      temp_records        = TempRecords,
	      temp_contracts      = TempContracts,
	      temp_callbacks      = TempCallbacks}.

-spec delete(codeserver()) -> 'ok'.

delete(CServer) ->
  lists:foreach(fun(Table) -> true = ets:delete(Table) end, tables(CServer)).

-spec insert(atom(), cerl:c_module(), codeserver()) -> codeserver().

insert(Mod, ModCode, CS) ->
  Name = cerl:module_name(ModCode),
  Exports = cerl:module_exports(ModCode),
  Attrs = cerl:module_attrs(ModCode),
  Defs = cerl:module_defs(ModCode),
  {Files, SmallDefs} = compress_file_anno(Defs),
  As = cerl:get_ann(ModCode),
  Funs =
    [{{Mod, cerl:fname_id(Var), cerl:fname_arity(Var)},
      Val, {Var, cerl_trees:get_label(Fun)}} || Val = {Var, Fun} <- SmallDefs],
  Keys = [Key || {Key, _Value, _Label} <- Funs],
  ModEntry = {Mod, {Name, Exports, Attrs, Keys, As}},
  ModFileEntry = {{mod, Mod}, Files},
  true = ets:insert(CS#codeserver.code, [ModEntry, ModFileEntry|Funs]),
  CS.

-spec get_temp_exported_types(codeserver()) -> sets:set(mfa()).

get_temp_exported_types(#codeserver{temp_exported_types = TempExpTypes}) ->
  ets_set_to_set(TempExpTypes).

-spec insert_temp_exported_types(sets:set(mfa()), codeserver()) -> codeserver().

insert_temp_exported_types(Set, CS) ->
  TempExportedTypes = CS#codeserver.temp_exported_types,
  true = ets_set_insert_set(Set, TempExportedTypes),
  CS.

-spec insert_exports([mfa()], codeserver()) -> codeserver().

insert_exports(List, #codeserver{exports = Exports} = CS) ->
  true = ets_set_insert_list(List, Exports),
  CS.

-spec insert_fun_meta_info(fun_meta_info(), codeserver()) -> codeserver().

insert_fun_meta_info(List, #codeserver{fun_meta_info = FunMetaInfo} = CS) ->
  true = ets:insert(FunMetaInfo, List),
  CS.

-spec is_exported(mfa(), codeserver()) -> boolean().

is_exported(MFA, #codeserver{exports = Exports}) ->
  ets_set_is_element(MFA, Exports).

-spec get_exported_types_table(codeserver()) -> map_ets().

get_exported_types_table(#codeserver{exported_types = ExpTypes}) ->
  ExpTypes.

-spec extract_exported_types(codeserver()) -> {codeserver(), set_ets()}.

extract_exported_types(#codeserver{exported_types = ExpTypes} = CS) ->
  {CS#codeserver{exported_types = 'clean'}, ExpTypes}.

-spec get_exports(codeserver()) -> sets:set(mfa()).

get_exports(#codeserver{exports = Exports}) ->
  ets_set_to_set(Exports).

-spec finalize_exported_types(sets:set(mfa()), codeserver()) -> codeserver().

finalize_exported_types(Set,
                        #codeserver{exported_types = ExportedTypes,
                                    temp_exported_types = TempETypes} = CS) ->
  true = ets_set_insert_set(Set, ExportedTypes),
  true = ets:delete(TempETypes),
  CS#codeserver{temp_exported_types = clean}.

-spec lookup_mod_code(atom(), codeserver()) -> cerl:c_module().

lookup_mod_code(Mod, CS) when is_atom(Mod) ->
  table__lookup(CS#codeserver.code, Mod).

-spec lookup_mfa_code(mfa(), codeserver()) -> {cerl:c_var(), cerl:c_fun()}.

lookup_mfa_code({_M, _F, _A} = MFA, CS) ->
  table__lookup(CS#codeserver.code, MFA).

-spec lookup_mfa_var_label(mfa(), codeserver()) -> {cerl:c_var(), label()}.

lookup_mfa_var_label({_M, _F, _A} = MFA, CS) ->
  ets:lookup_element(CS#codeserver.code, MFA, 3).

-spec get_next_core_label(codeserver()) -> label().

get_next_core_label(#codeserver{next_core_label = NCL}) ->
  NCL.

-spec set_next_core_label(label(), codeserver()) -> codeserver().

set_next_core_label(NCL, CS) ->
  CS#codeserver{next_core_label = NCL}.

-spec lookup_mod_records(atom(), codeserver()) -> types().

lookup_mod_records(Mod, #codeserver{records = RecDict}) when is_atom(Mod) ->
  ets:lookup_element(RecDict, Mod, 2, #{}).

-spec get_records_table(codeserver()) -> map_ets().

get_records_table(#codeserver{records = RecDict}) ->
  RecDict.

-spec extract_records(codeserver()) -> {codeserver(), map_ets()}.

extract_records(#codeserver{records = RecDict} = CS) ->
  {CS#codeserver{records = clean}, RecDict}.

-spec store_temp_records(module(), types(), codeserver()) -> codeserver().

store_temp_records(Mod, Map, #codeserver{temp_records = TempRecDict} = CS)
  when is_atom(Mod) ->
  case maps:size(Map) =:= 0 of
    true -> CS;
    false -> CS#codeserver{temp_records = ets_map_store(Mod, Map, TempRecDict)}
  end.

-spec get_temp_records_table(codeserver()) -> map_ets().

get_temp_records_table(#codeserver{temp_records = TempRecDict}) ->
  TempRecDict.

-spec lookup_temp_mod_records(module(), codeserver()) -> types().

lookup_temp_mod_records(Mod, #codeserver{temp_records = TempRecDict}) ->
  ets:lookup_element(TempRecDict, Mod, 2, #{}).

-spec finalize_records(codeserver()) -> codeserver().

finalize_records(#codeserver{temp_records = TmpRecords,
                             records = Records} = CS) ->
  %% The annotations of the abstract code used to be reset before
  %% Erlang/OTP 24. However, the location is used in 24, why the
  %% annotations are no longer reset. The increased memory usage is
  %% small compared to overall memory consumption.
  List = dialyzer_utils:ets_tab2list(TmpRecords),
  true = ets:delete(TmpRecords),
  true = ets:insert(Records, List),
  CS#codeserver{temp_records = clean}.

-spec lookup_mod_contracts(atom(), codeserver()) -> contracts().

lookup_mod_contracts(Mod, #codeserver{contracts = ContDict})
  when is_atom(Mod) ->
  Keys = ets:lookup_element(ContDict, Mod, 2, []),
  #{Key => ets:lookup_element(ContDict, Key, 2) || Key <- Keys}.

-spec lookup_mfa_contract(mfa(), codeserver()) ->
         'error' | {'ok', dialyzer_contracts:file_contract()}.

lookup_mfa_contract(MFA, #codeserver{contracts = ContDict}) ->
  ets_dict_find(MFA, ContDict).

-spec lookup_meta_info(module() | mfa(), codeserver()) ->
                          {'ok', meta_info()} | 'error'.

lookup_meta_info(MorMFA, #codeserver{fun_meta_info = FunMetaInfo}) ->
  ets_dict_find(MorMFA, FunMetaInfo).

-spec is_member_meta_info(module() | mfa(), codeserver()) -> boolean().

is_member_meta_info(MorMFA, #codeserver{fun_meta_info = FunMetaInfo}) ->
  ets_set_is_element(MorMFA, FunMetaInfo).

-spec get_contracts(codeserver()) ->
                       dict:dict(mfa(), dialyzer_contracts:file_contract()).

get_contracts(#codeserver{contracts = ContDict}) ->
  dict:filter(fun({_M, _F, _A}, _) -> true;
                 (_, _) -> false
              end, ets_dict_to_dict(ContDict)).

-spec get_callbacks(codeserver()) -> list().

get_callbacks(#codeserver{callbacks = CallbDict}) ->
  ets:tab2list(CallbDict).

-spec store_temp_contracts(module(), contracts(), contracts(), codeserver()) ->
	 codeserver().

store_temp_contracts(Mod, SpecMap, CallbackMap,
		     #codeserver{temp_contracts = Cn,
				 temp_callbacks = Cb} = CS)
  when is_atom(Mod) ->
  %% Make sure Mod is stored even if there are no callbacks or
  %% contracts.
  CS1 = CS#codeserver{temp_contracts = ets_map_store(Mod, SpecMap, Cn)},
  CS1#codeserver{temp_callbacks = ets_map_store(Mod, CallbackMap, Cb)}.

-spec all_temp_modules(codeserver()) -> [module()].

all_temp_modules(#codeserver{temp_contracts = TempContTable}) ->
  ets:select(TempContTable, [{{'$1', '$2'}, [], ['$1']}]).

-spec store_contracts(module(), contracts(), contracts(), codeserver()) ->
                         codeserver().

store_contracts(Mod, SpecMap, CallbackMap, CS) ->
  #codeserver{contracts = SpecDict, callbacks = CallbackDict} = CS,
  Keys = maps:keys(SpecMap),
  true = ets:insert(SpecDict, maps:to_list(SpecMap)),
  true = ets:insert(SpecDict, {Mod, Keys}),
  true = ets:insert(CallbackDict, maps:to_list(CallbackMap)),
  CS.

-spec get_temp_contracts(module(), codeserver()) ->
                            {contracts(), contracts()}.

get_temp_contracts(Mod, #codeserver{temp_contracts = TempContDict,
                                    temp_callbacks = TempCallDict}) ->
  [{Mod, Contracts}] = ets:lookup(TempContDict, Mod),
  true = ets:delete(TempContDict, Mod),
  [{Mod, Callbacks}] = ets:lookup(TempCallDict, Mod),
  true = ets:delete(TempCallDict, Mod),
  {Contracts, Callbacks}.

-spec give_away(codeserver(), pid()) -> 'ok'.

give_away(CServer, Pid) ->
  lists:foreach(fun(Table) -> true = ets:give_away(Table, Pid, any)
                end, tables(CServer)).

tables(#codeserver{code = Code,
                   fun_meta_info = FunMetaInfo,
                   exports = Exports,
                   temp_exported_types = TempExpTypes,
                   temp_records = TempRecords,
                   temp_contracts = TempContracts,
                   temp_callbacks = TempCallbacks,
                   exported_types = ExportedTypes,
                   records = Records,
                   contracts = Contracts,
                   callbacks = Callbacks}) ->
  [Table ||  Table <- [Code, FunMetaInfo, Exports, TempExpTypes,
                       TempRecords, TempContracts, TempCallbacks,
                       ExportedTypes, Records, Contracts, Callbacks],
             Table =/= clean].

-spec finalize_contracts(codeserver()) -> codeserver().

finalize_contracts(#codeserver{temp_contracts = TempContDict,
                               temp_callbacks = TempCallDict} = CS)  ->
  true = ets:delete(TempContDict),
  true = ets:delete(TempCallDict),
  CS#codeserver{temp_contracts = clean, temp_callbacks = clean}.

-spec translate_fake_file(codeserver(), module(), file:filename()) ->
                             file:filename().

translate_fake_file(#codeserver{code = Code}, Module, FakeFile) ->
  Files = ets:lookup_element(Code, {mod, Module}, 2),
  {FakeFile, File} = lists:keyfind(FakeFile, 1, Files),
  File.

table__lookup(TablePid, M) when is_atom(M) ->
  {Name, Exports, Attrs, Keys, As} = ets:lookup_element(TablePid, M, 2),
  Defs = [table__lookup(TablePid, Key) || Key <- Keys],
  cerl:ann_c_module(As, Name, Exports, Attrs, Defs);
table__lookup(TablePid, MFA) ->
  ets:lookup_element(TablePid, MFA, 2).

compress_file_anno(Term) ->
  {Files, SmallTerm} = compress_file_anno(Term, []),
  {[{FakeFile, File} || {File, {file, FakeFile}} <- Files], SmallTerm}.

compress_file_anno({file, F}, Fs) when is_list(F) ->
  case lists:keyfind(F, 1, Fs) of
    false ->
      I = integer_to_list(length(Fs)),
      FileI = {file, I},
      NFs = [{F, FileI}|Fs],
      {NFs, FileI};
    {F, FileI} -> {Fs, FileI}
  end;
compress_file_anno(T, Fs) when is_tuple(T) ->
  {NFs, NL} = compress_file_anno(tuple_to_list(T), Fs),
  {NFs, list_to_tuple(NL)};
compress_file_anno([E|L], Fs) ->
  {Fs1, NE} = compress_file_anno(E, Fs),
  {NFs, NL} = compress_file_anno(L, Fs1),
  {NFs, [NE|NL]};
compress_file_anno(T, Fs) -> {Fs, T}.
