/-
Copyright (c) 2016 Microsoft Corporation. All rights reserved.
Released under Apache 2.0 license as described in the file LICENSE.
Authors: Leonardo de Moura

Helper tactic for showing that a type has decidable equality.
-/
prelude
import init.meta.contradiction_tactic init.meta.constructor_tactic
import init.meta.injection_tactic init.meta.relation_tactics
import init.meta.rec_util init.meta.interactive

namespace tactic
open expr environment list

/- Retrieve the name of the type we are building a decidable equality proof for. -/
private meta def get_dec_eq_type_name : tactic name :=
do {
  (pi x1 i1 d1 (pi x2 i2 d2 b)) ← target >>= whnf,
  (const n ls) ← return (get_app_fn b),
  when (n ≠ `decidable) failed,
  (const I ls) ← return (get_app_fn d1),
  return I }
<|>
fail "mk_dec_eq_instance tactic failed, target type is expected to be of the form (decidable_eq ...)"

/- Extract (lhs, rhs) from a goal (decidable (lhs = rhs)) -/
private meta def get_lhs_rhs : tactic (expr × expr) :=
do
  (app dec lhs_eq_rhs) ← target | fail "mk_dec_eq_instance failed, unexpected case",
  match_eq lhs_eq_rhs

private meta def find_next_target : list expr → list expr → tactic (expr × expr)
| (t::ts) (r::rs) := if t = r then find_next_target ts rs else return (t, r)
| l1       l2     := failed

/- Create an inhabitant of (decidable (lhs = rhs)) -/
private meta def mk_dec_eq_for (lhs : expr) (rhs : expr) : tactic expr :=
do lhs_type ← infer_type lhs,
   dec_type ← mk_app `decidable_eq [lhs_type] >>= whnf,
   do {
     inst ← mk_instance dec_type,
     return $ inst lhs rhs }
   <|>
   do {
     f ← pp dec_type,
     fail $ to_fmt "mk_dec_eq_instance failed, failed to generate instance for" ++ format.nest 2 (format.line ++ f) }

/- Target is of the form (decidable (C ... = C ...)) where C is a constructor -/
private meta def dec_eq_same_constructor : name → name → nat → tactic unit
| I_name F_name num_rec :=
do
  (lhs, rhs) ← get_lhs_rhs,
  -- Try easy case first, where the proof is just reflexivity
  (unify lhs rhs >> right >> reflexivity)
  <|>
  do {
    let lhs_list := get_app_args lhs,
    let rhs_list := get_app_args rhs,
    when (length lhs_list ≠ length rhs_list) (fail "mk_dec_eq_instance failed, constructor applications have different number of arguments"),
    (lhs_arg,  rhs_arg) ← find_next_target lhs_list rhs_list,
    rec ← is_type_app_of lhs_arg I_name,
    inst ← if rec then do {
      inst_fn ← mk_brec_on_rec_value F_name num_rec,
      return $ app inst_fn rhs_arg }
    else do {
      mk_dec_eq_for lhs_arg rhs_arg
    },
    `[apply @decidable.by_cases _ _ %%inst],
    -- discharge first (positive) case by recursion
    intro1 >>= subst >> dec_eq_same_constructor I_name F_name (if rec then num_rec + 1 else num_rec),
    -- discharge second (negative) case by contradiction
    intro1, left, -- decidable.is_false
    intro1 >>= injection,
    intros, contradiction,
    return () }

/- Easy case: target is of the form (decidable (C_1 ... = C_2 ...)) where C_1 and C_2 are distinct constructors -/
private meta def dec_eq_diff_constructor : tactic unit :=
left >> intron 1 >> contradiction

/- This tactic is invoked for each case of decidable_eq. There n^2 cases, where n is the number
   of constructors. -/
private meta def dec_eq_case_2 (I_name : name) (F_name : name) : tactic unit :=
do
  (lhs, rhs)       ← get_lhs_rhs,
  let lhs_fn      := get_app_fn lhs,
  let rhs_fn      := get_app_fn rhs,
  if lhs_fn = rhs_fn
  then dec_eq_same_constructor I_name F_name 0
  else dec_eq_diff_constructor

private meta def dec_eq_case_1 (I_name : name) (F_name : name) : tactic unit :=
intro `w >>= cases >> all_goals (dec_eq_case_2 I_name F_name)

meta def mk_dec_eq_instance_core : tactic unit :=
do I_name ← get_dec_eq_type_name,
   env ← get_env,
   let v_name := `_v,
   let F_name := `_F,
   let num_indices := inductive_num_indices env I_name,
   let idx_names   := list.map (λ (p : name × nat), mk_num_name p~>fst p~>snd) (list.zip (list.repeat `idx num_indices) (list.iota num_indices)),
   -- Use brec_on if type is recursive.
   -- We store the functional in the variable F.
   if is_recursive env I_name
   then intro1 >>= (λ x, induction x (idx_names ++ [v_name, F_name]) (some $ I_name <.> "brec_on") >> return ())
   else intro v_name >> return (),
   -- Apply cases to first element of type (I ...)
   get_local v_name >>= cases,
   all_goals (dec_eq_case_1 I_name F_name)

meta def mk_dec_eq_instance : tactic unit :=
do env ← get_env,
   (pi x1 i1 d1 (pi x2 i2 d2 b)) ← target >>= whnf,
   (const I_name ls) ← return (get_app_fn d1),
   when (is_ginductive env I_name ∧ ¬ is_inductive env I_name) $
      do { d1' ← whnf d1,
           (app I_basic_const I_idx) ← return d1',
           I_idx_type ← infer_type I_idx,
           new_goal ← to_expr ``(∀ (_idx : %%I_idx_type), decidable_eq (%%I_basic_const _idx)),
           assert `_basic_dec_eq new_goal,
           swap,
           to_expr `(_basic_dec_eq %%I_idx) >>= exact,
           intro1,
           return () },
   mk_dec_eq_instance_core

meta instance binder_info.has_decidable_eq : decidable_eq binder_info :=
by mk_dec_eq_instance

end tactic

/- instances of types in dependent files -/
instance : decidable_eq ordering :=
by tactic.mk_dec_eq_instance
