(* -------------------------------------------------------------------- *)
open EcUtils
open EcMaps
open EcPath
open EcAst
open EcTypes
open EcFol
open EcEnv
open EcAlgebra
open EcAlgTactic
open EcCoreGoal
open EcLowGoal
open FApi

(* -------------------------------------------------------------------- *)
type norm_kind =
  | NKring  of cring * RState.rstate ref
  | NKfield of cfield * RState.rstate ref
  | NKdefault

type einfo = {
  i_env    : env;
  kind_tbl : norm_kind Hty.t;
  rw_info  : path list;
  inj_info : Sp.t;
}

(* -------------------------------------------------------------------- *)
let algerror tc =
  tc_error !!tc "cannot solve goal with ring/field/algebra"

(* -------------------------------------------------------------------- *)
let prewrite =
  EcPath.extend EcCoreLib.p_top ["Ring"; "rw_algebra"]

let prewrite_inj =
  EcPath.extend EcCoreLib.p_top ["Ring"; "inj_algebra"]

let get_injection env p =
  try
    let ax = EcEnv.Ax.by_path p env in
    (* try to match a term of the form  _ = _ <=> inj _ = inj _ *)
    let _, concl = decompose_forall ax.EcDecl.ax_spec in
    let _, f = destr_iff concl in
    let f1, f2 = destr_eq f in
    match f1.f_node, f2.f_node with
    | Fapp({f_node = Fop(op1,_)}, [_]), Fapp({f_node = Fop(op2,_)}, [_]) when
        EcPath.p_equal op1 op2 ->
      Some(op1, p)
    | _ -> None
  with DestrError _ -> None

let init_einfo env =
  let rw = Sp.elements (EcEnv.BaseRw.by_path prewrite env) in
  let inj = Sp.elements (EcEnv.BaseRw.by_path prewrite_inj env) in
  let inj = List.pmap (get_injection env) inj in
  let inj_rw = List.map snd inj in
  let inj_set = List.fold_left (fun s (p,_) -> Sp.add p s) Sp.empty inj in
  { i_env    = env;
    kind_tbl = Hty.create 23;
    rw_info  = inj_rw @ rw;
    inj_info = inj_set
  }

let is_inj info f =
  match f.f_node with
  | Fop(p,_) -> Sp.mem p info.inj_info
  | _ -> false

let get_field env hyps ty () =
  let tparams = (LDecl.tohyps hyps).EcBaseLogic.h_tvar in
  match EcTyping.get_field (tparams, ty) env with
  | Some f ->
    let cr = cfield_of_field f in
    let m  = ref RState.empty in
    Some (NKfield(cr,m))
  | None -> None

let get_ring env hyps ty () =
  let tparams = (LDecl.tohyps hyps).EcBaseLogic.h_tvar in
  match EcTyping.get_ring (tparams,ty) env with
  | Some r ->
    let cr = cring_of_ring r in
    let m = ref RState.empty in
    Some (NKring(cr,m))
  | None -> None

let norm_kind einfo hyps ty =
  try Hty.find einfo.kind_tbl ty
  with Not_found ->
    let kind =
      odfl NKdefault
        (List.fpick
           [get_field einfo.i_env hyps ty;
            get_ring einfo.i_env hyps ty]) in
    Hty.add einfo.kind_tbl ty kind;
    kind

let t_reflex_assumption g =
  t_ors [t_reflex;
              t_assumption `Alpha ;
              t_seq t_symmetry (t_assumption `Alpha)] g

let t_intro_eq g = t_intros_s (`Symbol ["_"]) g

let is_in_hyps hyps f1 f2 =
  let is_in hyps f1 f2 =
    try ignore (alpha_find_in_hyps hyps (f_eq f1 f2)); true
    with Not_found -> false in
  EcReduction.is_alpha_eq hyps f1 f2 ||
    is_in hyps f1 f2 || is_in hyps f2 f1

let t_ring r l (f1,f2) g =
  t_seq (t_ring r l (f1,f2)) algerror g

let destr_neq f =
  match sform_of_form f with
  | SFnot f ->
    begin match sform_of_form f with
    | SFeq(f1,f2) -> Some(f1,f2)
    | _ -> None
    end
  | _ -> None

let t_field_neq r g =
  let env, hyps, concl = tc1_eflat g in
  match destr_neq concl with
  | Some (f1,f2) ->
    let ty = f1.f_ty in
    let h = EcEnv.LDecl.fresh_id hyps "_" in
    let can_app (id,lk) =
      match lk with
      | EcBaseLogic.LD_hyp f ->
        begin match destr_neq f with
        | Some (f1',f2') when EcReduction.EqTest.for_type env f1'.f_ty ty ->
          Some (t_seqsub
                  (t_apply_s EcCoreLib.CI_Logic.p_negbTE [] ~args:[f_eq f1' f2'] ~sk:2)
                  [ t_apply_hyp id;
                    t_ring r [(f1,f2)] (f1', f2')])
        | _ -> None
        end
      | _ -> None in
    let tacs = List.rev_pmap can_app (EcEnv.LDecl.tohyps hyps).EcBaseLogic.h_local in
    t_try (t_seqs [
      (fun g -> !@ (t_change1 (f_imp (f_eq f1 f2) f_false) g));
      t_intros_i [h];
      t_ors tacs]) g
  | _ -> algerror g

let t_field r l (f1,f2) g =
  t_seq (t_field r l (f1,f2)) (t_field_neq r.EcDecl.f_ring) g

let autorewrite info f1 f2 g =
  let res = ref (f_true, f_true) in
  let t_get_res g =
    res := destr_eq (tc1_goal g);
    t_id g in
  let _ =
    t_first
      (fun g -> t_first t_get_res (EcHiGoal.LowRewrite.t_autorewrite info.rw_info g))
      (t_cut (f_eq f1 f2) g) in
  !res

let t_autorw info g =
  EcHiGoal.LowRewrite.t_autorewrite info.rw_info g

let rec t_alg_eq info g =
  let f1, f2 =
    try destr_eq (tc1_goal g)
    with EcFol.DestrError _ -> raise InvalidGoalShape
  in
  t_cut_alg_eq t_reflex_assumption info f1 f2 g

and t_cut_alg_eq t_cont info f1 f2 g =
  let hyps = tc1_hyps g in
  if is_in_hyps hyps f1 f2 then t_cont g
  else
    let f1', f2' = autorewrite info f1 f2 g in
    let t_cont =
      if f_equal f1 f1' && f_equal f2 f2' then t_cont
      else
        fun g ->
          t_seqsub (t_cut (f_eq f1 f2))
            [ (fun g -> t_first t_reflex_assumption (t_autorw info g));
              t_seq t_intro_eq t_cont] g in
    t_cut_alg_eq1 t_cont info f1' f2' g

and t_cut_alg_eq1 t_cont info f1 f2 g =
  let hyps = tc1_hyps g in
  if is_in_hyps hyps f1 f2 then t_cont g
  else
    match norm_kind info hyps f1.f_ty with
    | NKring(cr,m)  -> t_cut_ring_eq t_cont info cr m f1 f2 g
    | NKfield(cr,m) -> t_cut_field_eq t_cont info cr m f1 f2 g
    | NKdefault     -> t_cut_subterm_eq2 t_cont info f1 f2 g

and t_cut_alg_eqs t_cont info fs1 fs2 g =
  match fs1, fs2 with
  | [], [] -> t_cont g
  | f1::fs1, f2::fs2 ->
    t_cut_alg_eq (t_cut_alg_eqs t_cont info fs1 fs2) info f1 f2 g
  | _, _ -> assert false

and t_cut_subterm_eq t_cont info f1 f2 g =
  let hyps = tc1_hyps g in
  if is_in_hyps hyps f1 f2 then t_cont g
  else t_cut_subterm_eq1 t_cont info f1 f2 g

and t_cut_subterm_eq1 t_cont info f1 f2 g =
  let f1', f2' = autorewrite info f1 f2 g in
  if f_equal f1 f1' && f_equal f2 f2' then
    t_cut_subterm_eq2 t_cont info f1' f2' g
  else
    let t_cont g =
      t_seqsub (t_cut (f_eq f1 f2))
        [ (fun g -> t_first t_reflex_assumption (t_autorw info g));
          t_seq t_intro_eq t_cont] g in
    t_cut_alg_eq1 t_cont info f1' f2' g

and t_cut_subterm_eq2 t_cont info f1 f2 g =
  let hyps = tc1_hyps g in
  if is_in_hyps hyps f1 f2 then t_cont g
  else match f1.f_node, f2.f_node with
  | Fapp(op1,fs1), Fapp(op2,fs2) ->
    let hyps = tc1_hyps g in
    if EcReduction.is_alpha_eq hyps op1 op2 &&
      List.length fs1 = List.length fs2 then
      let t_cont g =
        (t_seqsub (t_cut (f_eq f1 f2)) [
          t_seq
            (t_congr (op1,op2) (List.combine fs1 fs2, f1.f_ty))
            t_reflex_assumption;
          t_seq t_intro_eq t_cont]) g in
      if is_inj info op1 then
        t_cut_subterm_eqs2 t_cont info fs1 fs2 g
      else
        t_cut_alg_eqs t_cont info fs1 fs2 g
    else algerror g
  | Ftuple fs1, Ftuple fs2 when List.length fs1 = List.length fs2 ->
    let t_cont g =
      (t_seqsub (t_cut (f_eq f1 f2)) [
        t_seq t_split t_reflex_assumption;
        t_seq t_intro_eq t_cont]) g in
    t_cut_alg_eqs t_cont info fs1 fs2 g
  (* TODO : add something for if ? *)
  | _, _ -> algerror g

and t_cut_subterm_eqs2 t_cont info fs1 fs2 g =
  match fs1, fs2 with
  | [], [] -> t_cont g
  | f1::fs1, f2::fs2 ->
    t_cut_subterm_eq2 (t_cut_subterm_eqs2 t_cont info fs1 fs2) info f1 f2 g
  | _, _ -> assert false

and t_cut_field_eq t_cont info cr rm f1 f2 g =
  let hyps = tc1_hyps g in
  let pe1, rm' = tofield hyps cr !rm f1 in
  let pe2, rm' = tofield hyps cr rm' f2 in
  rm := rm';
  let (_,(pe,_)) = field_simplify_pe cr [] (EcField.FEsub(pe1,pe2)) in
  let fv = Sint.elements (EcRing.fv_pe pe) in
  let r = field_of_cfield cr in
  if fv = [] then
    t_seqsub (t_cut (f_eq f1 f2))
      [ t_field r [] (f1,f2);
        t_seq t_intro_eq t_cont ] g
  else
    let t_end li lf' g =
      if li = [] then algerror g
      else
        let rm' = RState.update !rm li lf' in
        let f1' = offield r rm' pe1 in
        let f2' = offield r rm' pe2 in
        t_seqsub (t_cut (f_eq f1 f2))
          [t_seqsub (t_transitivity f1')
              [t_seq (t_field_congr cr !rm li lf') t_reflex_assumption;
               t_seqsub (t_transitivity f2')
                 [t_field r [] (f1',f2');
                  t_seq t_symmetry
                    (t_seq (t_field_congr cr !rm li lf')
                       t_reflex_assumption)
                 ]
              ];
           t_seq t_intro_eq t_cont] g in
    t_cut_merges t_end info rm' fv [emb_fzero r; emb_fone r] g

and t_cut_ring_eq t_cont info cr rm f1 f2 g =
  let hyps = tc1_hyps g in
  let pe1, rm' = toring hyps cr !rm f1 in
  let pe2, rm' = toring hyps cr rm' f2 in
  rm := rm';
  let pe = ring_simplify_pe cr [] (EcRing.PEsub(pe1,pe2)) in
  let fv = Sint.elements (EcRing.fv_pe pe) in
  let r = ring_of_cring cr in
  if fv = [] then
    t_seqsub (t_cut (f_eq f1 f2))
      [ t_ring r [] (f1,f2);
        t_seq t_intro_eq t_cont ] g
  else
    let t_end li lf' g =
      if li = [] then algerror g
      else
        let rm' = RState.update !rm li lf' in
        let f1' = ofring r rm' pe1 in
        let f2' = ofring r rm' pe2 in
        t_seqsub (t_cut (f_eq f1 f2))
          [t_seqsub (t_transitivity f1')
              [t_seq (t_ring_congr cr !rm li lf') t_reflex_assumption;
               t_seqsub (t_transitivity f2')
                 [t_ring r [] (f1',f2');
                  t_seq t_symmetry
                    (t_seq (t_ring_congr cr !rm li lf') t_reflex_assumption)
                 ]
              ];
           t_seq t_intro_eq t_cont] g in
    t_cut_merges t_end info rm' fv  [emb_rzero r; emb_rone r] g

and t_cut_merges t_end info rm fv lv g =
  let m = ref Mint.empty in
  let t_end g =
    let li, lf' = List.split (Mint.bindings !m) in
    t_end li lf' g in

  let t_unify1 t_cont i1 f1 f2 g =
    let t_cont g = m := Mint.add i1 f2 !m; t_cont g in
    t_cut_subterm_eq t_cont info f1 f2 g in

  let tomatch = ref lv in
  let t_tomatch t_cont i1 f1 g =
    let rec t_match l g =
      match l with
      | [] -> tomatch := f1 :: !tomatch; t_cont g
      | f2::l -> t_or (t_unify1 t_cont i1 f1 f2) (t_match l) g in
    t_match !tomatch g in

  let rec t_aux li g =
    match li with
    | [] -> t_end g
    | i :: li -> t_tomatch (t_aux li) i (oget (RState.get i rm)) g in

  t_aux fv g

let t_alg_eq g =
  let env = tc1_env g in
  let info = init_einfo env in
  t_alg_eq info g
