(*
	The Haxe Compiler
	Copyright (C) 2005-2019  Haxe Foundation

	This program is free software; you can redistribute it and/or
	modify it under the terms of the GNU General Public License
	as published by the Free Software Foundation; either version 2
	of the License, or (at your option) any later version.

	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 General Public License for more details.

	You should have received a copy of the GNU General Public License
	along with this program; if not, write to the Free Software
	Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
*)
open Common
open Globals
open Ast
open Type
open Codegen
open Texpr.Builder
open Gencommon

(* ******************************************* *)
(* EnumToClass *)
(* ******************************************* *)
(*
	For languages that don't support parameterized enums and/or metadata in enums, we need to transform
	enums into normal classes. This is done at the first module pass by creating new classes with the same
	path inside the modules, and removing the actual enum module by setting it as en extern.

	* The target must create its own strategy to deal with reflection. As it is right now, we will have a base class
	which the class will extend, create @:$IsEnum metadata for the class, and create @:alias() metadatas for the fields,
	with their tag order (as a string) as their alias. If you are using ReflectionCFs, then you don't have to worry
	about that, as it's already generating all information needed by the haxe runtime.
	so they can be
*)
let name = "enum_to_class"
let priority = solve_deps name []

type t = {
	ec_tbl : (path, tclass) Hashtbl.t;
}

let new_t () = {
	ec_tbl = Hashtbl.create 10
}

(* ******************************************* *)
(* EnumToClassModf *)
(* ******************************************* *)
(*
	The actual Module Filter that will transform the enum into a class

	dependencies:
		Should run before ReflectionCFs, in order to enable proper reflection access.
		Should run before RealTypeParams.RealTypeParamsModf, since generic enums must be first converted to generic classes
		It needs that the target platform implements __array__() as a shortcut to declare haxe.ds.Vector
*)
module EnumToClassModf =
struct
	let name = "enum_to_class_mod"
	let priority = solve_deps name [DBefore ReflectionCFs.priority; DBefore RealTypeParams.RealTypeParamsModf.priority]

	let pmap_exists fn pmap = try PMap.iter (fun a b -> if fn a b then raise Exit) pmap; false with | Exit -> true

	let has_any_meta en =
		let has_meta meta = List.exists (fun (m,_,_) -> match m with Meta.Custom _ -> true | _ -> false) meta in
		has_meta en.e_meta || pmap_exists (fun _ ef -> has_meta ef.ef_meta) en.e_constrs

	let convert gen t base_class base_param_class en =
		let handle_type_params = false in (* TODO: look into this *)
		let basic = gen.gcon.basic in
		let pos = en.e_pos in

		(* create the class *)
		let cl = mk_class en.e_module en.e_path pos in
		Hashtbl.add t.ec_tbl en.e_path cl;

		(match Texpr.build_metadata gen.gcon.basic (TEnumDecl en) with
			| Some expr ->
				let cf = mk_class_field ~static:true "__meta__" expr.etype false expr.epos (Var { v_read = AccNormal; v_write = AccNormal }) [] in
				cf.cf_expr <- Some expr;
				cl.cl_statics <- PMap.add "__meta__" cf cl.cl_statics;
				cl.cl_ordered_statics <- cf :: cl.cl_ordered_statics
			| _ -> ()
		);

		let super, has_params = if Meta.has Meta.FlatEnum en.e_meta then base_class, false else base_param_class, true in

		cl.cl_super <- Some(super,[]);
		if en.e_extern then add_class_flag cl CExtern;
		en.e_meta <- (Meta.Class, [], pos) :: en.e_meta;
		cl.cl_module <- en.e_module;
		cl.cl_meta <- ( Meta.Enum, [], pos ) :: cl.cl_meta;

		(match gen.gcon.platform with
			| Cs when Common.defined gen.gcon Define.CoreApiSerialize ->
				cl.cl_meta <- ( Meta.Meta, [ (EField( (EConst (Ident "System"), null_pos ), "Serializable" ), null_pos) ], null_pos ) :: cl.cl_meta
			| _ -> ());
		let c_types =
			if handle_type_params then
				List.map (fun (s,t) -> (s, TInst (map_param (get_cl_t t), []))) en.e_params
			else
				[]
		in

		cl.cl_params <- c_types;

		let i = ref 0 in
		let cfs = List.map (fun name ->
			let ef = PMap.find name en.e_constrs in
			let pos = ef.ef_pos in
			let old_i = !i in
			incr i;

			let cf = match follow ef.ef_type with
				| TFun(params,ret) ->
					let dup_types =
						if handle_type_params then
							List.map (fun (s,t) -> (s, TInst (map_param (get_cl_t t), []))) en.e_params
						else
							[]
					in

					let ef_type =
						let fn, types = if handle_type_params then snd, dup_types else (fun _ -> t_dynamic), en.e_params in
						let t = apply_params en.e_params (List.map fn types) ef.ef_type in
						apply_params ef.ef_params (List.map fn ef.ef_params) t
					in

					let params, ret = get_fun ef_type in
					let cf_params = if handle_type_params then dup_types @ ef.ef_params else [] in

					let cf = mk_class_field name ef_type true pos (Method MethNormal) cf_params in
					cf.cf_meta <- [];

					let tf_args = List.map (fun (name,opt,t) ->  (alloc_var name t, if opt then Some (Texpr.Builder.make_null t null_pos) else None) ) params in
					let arr_decl = mk_nativearray_decl gen t_dynamic (List.map (fun (v,_) -> mk_local v pos) tf_args) pos in
					let expr = {
						eexpr = TFunction({
							tf_args = tf_args;
							tf_type = ret;
							tf_expr = mk_block ( mk_return { eexpr = TNew(cl,List.map snd dup_types, [make_int gen.gcon.basic old_i pos; arr_decl] ); etype = TInst(cl, List.map snd dup_types); epos = pos } );
						});
						etype = ef_type;
						epos = pos
					} in
					cf.cf_expr <- Some expr;
					cf
				| _ ->
					let actual_t = match follow ef.ef_type with
						| TEnum(e, p) -> TEnum(e, List.map (fun _ -> t_dynamic) p)
						| _ -> die "" __LOC__
					in
					let cf = mk_class_field name actual_t true pos (Var { v_read = AccNormal; v_write = AccNever }) [] in
					let args = if has_params then
						[make_int gen.gcon.basic old_i pos; null (gen.gclasses.nativearray t_dynamic) pos]
					else
						[make_int gen.gcon.basic old_i pos]
					in
					cf.cf_meta <- [Meta.ReadOnly,[],pos];
					cf.cf_expr <- Some {
						eexpr = TNew(cl, List.map (fun _ -> t_empty) cl.cl_params, args);
						etype = TInst(cl, List.map (fun _ -> t_empty) cl.cl_params);
						epos = pos;
					};
					cf
			in
			cl.cl_statics <- PMap.add cf.cf_name cf cl.cl_statics;
			cf
		) en.e_names in
		let constructs_cf = mk_class_field ~static:true "__hx_constructs" (gen.gclasses.nativearray basic.tstring) true pos (Var { v_read = AccNormal; v_write = AccNever }) [] in
		constructs_cf.cf_meta <- [Meta.ReadOnly,[],pos];
		constructs_cf.cf_expr <- Some (mk_nativearray_decl gen basic.tstring (List.map (fun s -> { eexpr = TConst(TString s); etype = basic.tstring; epos = pos }) en.e_names) pos);

		cl.cl_ordered_statics <- constructs_cf :: cfs @ cl.cl_ordered_statics ;
		cl.cl_statics <- PMap.add "__hx_constructs" constructs_cf cl.cl_statics;

		let getTag_cf_type = tfun [] basic.tstring in
		let getTag_cf = mk_class_field "getTag" getTag_cf_type true pos (Method MethNormal) [] in
		add_class_field_flag getTag_cf CfFinal;
		getTag_cf.cf_expr <- Some {
			eexpr = TFunction {
				tf_args = [];
				tf_type = basic.tstring;
				tf_expr = mk_return (
					let e_constructs = mk_static_field_access_infer cl "__hx_constructs" pos [] in
					let e_this = mk (TConst TThis) (TInst (cl,[])) pos in
					let e_index = mk_field_access gen e_this "index" pos in
					{
						eexpr = TArray(e_constructs,e_index);
						etype = basic.tstring;
						epos = pos;
					}
				)
			};
			etype = getTag_cf_type;
			epos = pos;
		};

		cl.cl_ordered_fields <- getTag_cf :: cl.cl_ordered_fields ;
		cl.cl_fields <- PMap.add "getTag" getTag_cf cl.cl_fields;
		add_class_field_flag getTag_cf CfOverride;
		cl.cl_meta <- (Meta.NativeGen,[],cl.cl_pos) :: cl.cl_meta;
		gen.gadd_to_module (TClassDecl cl) (max_dep);

		TEnumDecl en

	(*
		traverse
			gen - gen context
			convert_all : bool - should we convert all enums? If set, convert_if_has_meta will be ignored.
			convert_if_has_meta : bool - should we convert only if it has meta?
			enum_base_class : tclass - the enum base class.
			should_be_hxgen : bool - should the created enum be hxgen?
	*)
	let configure gen t convert_all convert_if_has_meta enum_base_class param_enum_class =
		let convert e = convert gen t enum_base_class param_enum_class e in
		let run md =
			match md with
			| TEnumDecl e when is_hxgen md ->
				if convert_all then
					convert e
				else if convert_if_has_meta && has_any_meta e then
					convert e
				else if not (Meta.has Meta.FlatEnum e.e_meta) then
					convert e
				else begin
					(* take off the :hxgen meta from it, if there's any *)
					e.e_meta <- List.filter (fun (n,_,_) -> not (n = Meta.HxGen)) e.e_meta;
					md
				end
			| _ ->
				md
		in
		gen.gmodule_filters#add name (PCustom priority) run
end;;

(* ******************************************* *)
(* EnumToClassExprf *)
(* ******************************************* *)
(*
	Enum to class Expression Filter

	dependencies:
		Should run before TArrayTransform, since it generates array access expressions
*)
module EnumToClassExprf =
struct
	let name = "enum_to_class_exprf"
	let priority = solve_deps name [DBefore TArrayTransform.priority]

	let configure gen t mk_enum_index_call =
		let rec run e =
			let get_converted_enum_type et =
				let en, eparams = match follow (gen.gfollow#run_f et) with
					| TEnum(en,p) -> en, p
					| _ -> raise Not_found
				in
				let cl = Hashtbl.find t.ec_tbl en.e_path in
				TInst(cl, eparams)
			in

			match e.eexpr with
			| TEnumIndex f ->
				let f = run f in
				(try
					mk_field_access gen {f with etype = get_converted_enum_type f.etype} "index" e.epos
				with Not_found ->
					mk_enum_index_call f e.epos)
			| TCall (({eexpr = TField(_, FStatic({cl_path=[],"Type"},{cf_name="enumIndex"}))} as left), [f]) ->
				let f = run f in
				(try
					mk_field_access gen {f with etype = get_converted_enum_type f.etype} "index" e.epos
				with Not_found ->
					{ e with eexpr = TCall(left, [f]) })
			| TEnumParameter(f, _,i) ->
				let f = run f in
				(* check if en was converted to class *)
				(* if it was, switch on tag field and change cond type *)
				let f = try
					{ f with etype = get_converted_enum_type f.etype }
				with Not_found ->
					f
				in
				let cond_array = { (mk_field_access gen f "params" f.epos) with etype = gen.gclasses.nativearray t_dynamic } in
				index gen.gcon.basic cond_array i e.etype e.epos
			| _ ->
				Type.map_expr run e
		in
		gen.gexpr_filters#add name (PCustom priority) run

end;;

let configure gen convert_all convert_if_has_meta enum_base_class param_enum_class mk_enum_index_call =
	let t = new_t () in
	EnumToClassModf.configure gen t convert_all convert_if_has_meta enum_base_class param_enum_class;
	EnumToClassExprf.configure gen t mk_enum_index_call
