/-
Copyright (c) 2016 Gabriel Ebner. All rights reserved.
Released under Apache 2.0 license as described in the file LICENSE.
Authors: Gabriel Ebner
-/
import init.meta.tactic .utils .trim
open expr list tactic monad decidable

namespace super

meta def is_local_not (local_false : expr) (e : expr) : option expr :=
match e with
| (pi _ _ a b) := if b = local_false then some a else none
| _ := if local_false = ```(false) then is_not e else none
end

meta structure clause :=
(num_quants : ℕ)
(num_lits : ℕ)
(proof : expr)
(type : expr)
(local_false : expr)

namespace clause

meta def num_binders (c : clause) : ℕ := num_quants c + num_lits c

meta def inst (c : clause) (e : expr) : clause :=
(if num_quants c > 0
  then mk (num_quants c - 1) (num_lits c)
  else mk 0 (num_lits c - 1))
(app (proof c) e) (instantiate_var (binding_body (type c)) e) c^.local_false

meta def instn (c : clause) (es : list expr) : clause :=
foldr (λe c', inst c' e) c es

meta def open_const (c : clause) : tactic (clause × expr) := do
n ← mk_fresh_name,
b ← return $ local_const n (binding_name (type c)) (binding_info (type c)) (binding_domain (type c)),
return (inst c b, b)

meta def open_meta (c : clause) : tactic (clause × expr) := do
b ← mk_meta_var (binding_domain (type c)),
return (inst c b, b)

meta def close_const (c : clause) (e : expr) : clause :=
match e with
| local_const uniq pp info t :=
    let abst_type' := abstract_local (type c) (local_uniq_name e) in
    let type' := pi pp binder_info.default t (abstract_local (type c) uniq) in
    let abs_prf := abstract_local (proof c) uniq in
    let proof' := lambdas [e] c^.proof in
    if num_quants c > 0 ∨ has_var abst_type' then
      { c with num_quants := c^.num_quants + 1, proof := proof', type := type' }
    else
      { c with num_lits := c^.num_lits + 1, proof := proof', type := type' }
| _ := ⟨0, 0, default expr, default expr, default expr⟩
end

meta def open_constn : clause → ℕ → tactic (clause × list expr)
| c 0 := return (c, nil)
| c (n+1) := do
  (c', b) ← open_const c,
  (c'', bs) ← open_constn c' n,
  return (c'', b::bs)

meta def open_metan : clause → ℕ → tactic (clause × list expr)
| c 0 := return (c, nil)
| c (n+1) := do
  (c', b) ← open_meta c,
  (c'', bs) ← open_metan c' n,
  return (c'', b::bs)

meta def close_constn : clause → list expr → clause
| c [] := c
| c (b::bs') := close_const (close_constn c bs') b

private meta def parse_clause (local_false : expr) : expr → expr → tactic clause
| proof (pi n bi d b) := do
  lc_n ← mk_fresh_name,
  lc ← return $ local_const lc_n n bi d,
  c ← parse_clause (app proof lc) (instantiate_var b lc),
  return $ c^.close_const $ local_const lc_n n binder_info.default d
| proof ```(not %%formula) := parse_clause proof ```(%%formula → false)
| proof type :=
if type = local_false then do
  return { num_quants := 0, num_lits := 0, proof := proof, type := type, local_false := local_false }
else do
  univ ← infer_univ type,
  not_type ← return $ imp type local_false,
  parse_clause (lam `H binder_info.default not_type $ app (mk_var 0) proof) (imp not_type local_false)

meta def of_proof_and_type (local_false proof type : expr) : tactic clause :=
parse_clause local_false proof type

meta def of_proof (local_false proof : expr) : tactic clause := do
type ← infer_type proof,
of_proof_and_type local_false proof type

meta def of_classical_proof (proof : expr) : tactic clause :=
of_proof ```(false) proof

meta def inst_mvars (c : clause) : tactic clause := do
proof' ← instantiate_mvars (proof c),
type' ← instantiate_mvars (type c),
return { c with proof := proof', type := type' }

meta inductive literal
| left : expr → literal
| right : expr → literal

namespace literal

meta instance : decidable_eq literal := by mk_dec_eq_instance

meta def formula : literal → expr
| (left f) := f
| (right f) := f

meta def is_neg : literal → bool
| (left _) := tt
| (right _) := ff

meta def is_pos (l : literal) : bool := bnot l^.is_neg

meta def to_formula (l : literal) : expr :=
if l^.is_neg
then app (const ``not []) l^.formula
else formula l

meta def type_str : literal → string
| (literal.left _) := "left"
| (literal.right _) := "right"

meta instance : has_to_tactic_format literal :=
⟨λl, do
pp_f ← pp l^.formula,
return $ to_fmt l^.type_str ++ " (" ++ pp_f ++ ")"⟩

end literal

private meta def get_binding_body : expr → ℕ → expr
| e 0 := e
| e (i+1) := get_binding_body e^.binding_body i

meta def get_binder (e : expr) (i : nat) :=
binding_domain (get_binding_body e i)

meta def validate (c : clause) : tactic unit := do
concl ← return $ get_binding_body c^.type c^.num_binders,
unify concl c^.local_false
      <|> (do pp_concl ← pp concl, pp_lf ← pp c^.local_false,
              fail $ to_fmt "wrong local false: " ++ pp_concl ++ " =!= " ++ pp_lf),
type' ← infer_type c^.proof,
unify c^.type type' <|> (do pp_ty ← pp c^.type, pp_ty' ← pp type',
                           fail (to_fmt "wrong type: " ++ pp_ty ++ " =!= " ++ pp_ty'))

meta def get_lit (c : clause) (i : nat) : literal :=
let bind := get_binder (type c) (num_quants c + i) in
match is_local_not c^.local_false bind with
| some formula := literal.right formula
| none         := literal.left bind
end

meta def lits_where (c : clause) (p : literal → bool) : list nat :=
list.filter (λl, p (get_lit c l)) (range (num_lits c))

meta def get_lits (c : clause) : list literal :=
list.map (get_lit c) (range c^.num_lits)

private meta def tactic_format (c : clause) : tactic format := do
c ← c^.open_metan c^.num_quants,
pp (do l ← c.1^.get_lits, [l^.to_formula])

meta instance : has_to_tactic_format clause := ⟨tactic_format⟩

meta def is_maximal (gt : expr → expr → bool) (c : clause) (i : nat) : bool :=
list.empty (list.filter (λj, gt (get_lit c j)^.formula (get_lit c i)^.formula) (range c^.num_lits))

meta def normalize (c : clause) : tactic clause := do
opened  ← open_constn c (num_binders c),
lconsts_in_types ← return $ contained_lconsts_list (list.map local_type opened.2),
quants' ← return $ list.filter (λlc, rb_map.contains lconsts_in_types (local_uniq_name lc))
                                                      opened.2,
lits' ← return $ list.filter (λlc, ¬rb_map.contains lconsts_in_types (local_uniq_name lc))
                                                     opened.2,
return $ close_constn opened.1 (quants' ++ lits')

meta def whnf_head_lit (c : clause) : tactic clause := do
atom' ← whnf $ literal.formula $ get_lit c 0,
return $
if literal.is_neg (get_lit c 0) then
  { c with type := ```(%%atom' → %%(binding_body c^.type)) }
else
  { c with type := ```(not %%atom' → %%(c^.type^.binding_body)) }

end clause

meta def unify_lit (l1 l2 : clause.literal) : tactic unit :=
if clause.literal.is_pos l1 = clause.literal.is_pos l2 then
  unify (clause.literal.formula l1) (clause.literal.formula l2) transparency.none
else
  fail "cannot unify literals"

-- FIXME: this is most definitely broken with meta-variables that were already in the goal
meta def sort_and_constify_metas : list expr → tactic (list expr)
| exprs_with_metas := do
inst_exprs ← mapm instantiate_mvars exprs_with_metas,
metas ← return $ inst_exprs >>= get_metas,
match list.filter (λm, ¬has_meta_var (get_meta_type m)) metas with
| [] :=
     if metas^.empty then
       return []
     else do
       for' metas (λm, do trace (expr.to_string m), t ← infer_type m, trace (expr.to_string t)),
       fail "could not sort metas"
| ((mvar n t) :: _) := do
  c ← infer_type (mvar n t) >>= mk_local_def `x,
  unify c (mvar n t),
  rest ← sort_and_constify_metas metas,
  c ← instantiate_mvars c,
  return ([c] ++ rest)
| _ := failed
end

namespace clause

meta def meta_closure (metas : list expr) (qf : clause) : tactic clause := do
bs ← sort_and_constify_metas metas,
qf' ← clause.inst_mvars qf,
clause.inst_mvars $ clause.close_constn qf' bs

private meta def distinct' (local_false : expr) : list expr → expr → clause
| [] proof := ⟨ 0, 0, proof, local_false, local_false ⟩
| (h::hs) proof :=
  let (dups, rest) := partition (λh' : expr, h^.local_type = h'^.local_type) hs,
      proof_wo_dups := foldl (λproof (h' : expr),
                              instantiate_var (abstract_local proof h'^.local_uniq_name) h)
                         proof dups in
    (distinct' rest proof_wo_dups)^.close_const h

meta def distinct (c : clause) : tactic clause := do
(qf, vs) ← c^.open_constn c^.num_quants,
(fls, hs) ← qf^.open_constn qf^.num_lits,
return $ (distinct' c^.local_false hs fls^.proof)^.close_constn vs

end clause

end super
