#### The meta-language

#Negation
(Â¬)(p::Bool)::Bool = !p
#Disjunction
âˆ¨(p::Bool, q::Bool)::Bool =  p || q
#Conjunction
âˆ§(p::Bool, q::Bool)::Bool = Â¬(Â¬p âˆ¨ Â¬q)
#Implication
âŸ¹(p::Bool, q::Bool)::Bool = Â¬p âˆ¨ q
#Biconditional
âŸº(p::Bool, q::Bool)::Bool = (p âŸ¹ q) âˆ§ (q âŸ¹ p)

#The data-type for well-formed formulae
WFF = Union{Symbol, Expr}

#The language
ð�•ƒ = (:p, :q, :r)

"""
Generates all well-formed formulae of size `n`.

Sð�•ƒ(n) = Sð�•ƒ(n-1) âˆª {(Â¬s), (s âˆ¨ t) | s, t âˆˆ Sð�•ƒ(n-1)}
"""
function Sð�•ƒ(n::Int = 0)
    n == 0 ?
    [ð�•ƒ...] :
    [
    Sð�•ƒ(n-1);
    [:(Â¬$s) for s âˆˆ Sð�•ƒ(n-1)];
    [:($sâˆ¨$t) for s âˆˆ Sð�•ƒ(n-1) for t âˆˆ Sð�•ƒ(n-1)]
    ]
end

SL = Sð�•ƒ(3)

rand(SL)

"""
Rewrites a formula, replacing a given symbol with a boolean value (true / false).
"""
function rewrite!(formula::WFF, s::Symbol, v::Bool)
    for (i, e) in enumerate(formula.args)
        if e == s
            formula.args[i] = v
        elseif typeof(e) == Expr
            rewrite!(e, s, v)
        end
    end
end

#A demonstration of the `rewrite!` function
f = :((Â¬p âˆ¨ (q âˆ§ Â¬r)) âˆ¨ (p âŸ¹ Â¬p))
rewrite!(f, :p, true)
f

"""
Applies a given valuation to a formula `Ï†`.
"""
function evaluate(Ï†::WFF, v::Tuple{Vararg{Bool}})::Bool
    formula = deepcopy(Ï†)  #copying to preserve the input formula
    
    if typeof(Ï†) == Symbol
        formula = Expr(:call, :(x -> x), formula) #handling symbols
    end        

    for (val, s) âˆˆ zip(v, ð�•ƒ)
        rewrite!(formula, s, val)
    end

    eval(formula)
end

#A demonstration of the `evaluate` function
Ï• = :((Â¬r âŸ¹ (q âˆ§ p)) âŸº (((r âˆ¨ Â¬p) âˆ§ (p âŸ¹ q)) âˆ¨ p))
evaluate(Ï•, (true, false, true))

"""
Checks if a valuation v satisfies a given set of formulae.
"""
evaluate(formulae::Vector{Any}, v::Tuple{Vararg{Bool}})::Bool = reduce(âˆ§, [evaluate(formula, v) for formula âˆˆ formulae])

"""
Gives the list of all valuations that satisfy the given formula.
"""
function Î½(formula::Union{WFF, Vector{Expr}})::Vector{Tuple{Vararg{Bool}}}
    #the set of all possible valuations for the symbols in ð�•ƒ
    vs = Iterators.product(ntuple(i -> (false, true), length(ð�•ƒ))...) |> collect |> vec

    [v for v âˆˆ vs if evaluate(formula, v)]
end

"""
Checks if a proposition `t` is a logical consequence of a given set of propositions `S`.

In other words, this function checks if `S âŠ¨ t`.
"""
âŠ©(S::Vector{Any}, t::WFF)::Bool = Î½(S) âŠ† Î½(t)

#A demonstration of the `âŠ©` function
[:(p âˆ§ p), :(p âŸ¹ r)] âŠ© :(r âˆ¨ r)

"""
Checks if two propositional formulae `s` and `t` are logically equivalent (`s â‰¡ t`).
"""
â‰Œ(s::WFF, t::WFF)::Bool = (Î½(s) âŠ† Î½(t)) âˆ§ (Î½(t) âŠ† Î½(s))

#A demonstration of the `â‰Œ` function
:(p âˆ¨ Â¬(p âŸ¹ q)) â‰Œ :(p âˆ§ p)



#### Hilbert-style deductive calculus

#The Logical Axioms

LAâ‚�(s::WFF, t::WFF)::WFF = :( $s âŸ¹ ($t âŸ¹ $s) )

LAâ‚‚(s::WFF, t::WFF, u::WFF)::WFF = :( ($s âŸ¹ ($t âŸ¹ $u)) âŸ¹ (($s âŸ¹ $t) âŸ¹ ($s âŸ¹ $u)) )

LAâ‚ƒ(s::WFF)::WFF = :( Â¬Â¬$s âŸ¹ $s )

LAâ‚„(s::WFF, t::WFF)::WFF = :( (Â¬$s âŸ¹ Â¬$t) âŸ¹ ($t âŸ¹ $s) )

#Modus Ponens
MP(s::WFF, t::WFF)::WFF = s.args[begin] == :âŸ¹ && (s.args[begin+1] == t) ? s.args[end] : :missing

#Non-Logical Axioms
NLA(S   , n::Int)::WFF = S[n]

"""
A data structure defining sequents.

It contains the set of non-logical axioms `S`,
a propositional formula `t`,
a rule (logical axiom, rule of inference, or non-logical axiom) used to derive the proposition,
a list of arguments passed to the rule function.

`S âŠ¢ t`
"""
struct Sequent
    S::Vector{WFF}
    t::WFF
    rule::Function
    args::Vector{Any}

    #checks if the sequent rule gives the required formula
    Sequent(S, t, rule, args) = rule != MP && rule(args...) != t ?
        error("Inconsistent sequent") :
        new(S, t, rule, args)
end

#The data-type for formal proofs
FormalProof = Vector{Sequent}  #a FormalProof here is implemented as a list of Sequents

"""
A function that takes in a FormalProof (a list of sequents),
and verifies if the steps are correct.

If the formal proof is correct, the last propositional formula is returned.
"""
function verify(FP::FormalProof)
    for (i, Seq) âˆˆ enumerate(FP)
        if Seq.rule == MP
            #The indices here are relative positions
            m, n = Seq.args[begin]
            MP(FP[i - m].t, FP[i - n].t) == Seq.t ?
            println("Step $i involving MP is correct.") :
            error("Error in step $i in the formal proof.")
        end
    end

    println("\n The formal proof is correct!")

    return FP[end].t
end

#A demonstration of formal proof verification for the propositional formula `p âŸ¹ r`.
let S = [:(p âŸ¹ q), :(q âŸ¹ r)]
    proof::FormalProof = [
    Sequent(S, :(q âŸ¹ r), NLA, [S, 2]),
    Sequent(S, :(p âŸ¹ q), NLA, [S, 1]),
    Sequent(S, :((q âŸ¹ r) âŸ¹ (p âŸ¹ (q âŸ¹ r))), LAâ‚�, [:(q âŸ¹ r), :(p)]),
    Sequent(S, :((p âŸ¹ (q âŸ¹ r))), MP, [(1, 3)]),
    Sequent(S, :( (p âŸ¹ (q âŸ¹ r)) âŸ¹ ((p âŸ¹ q) âŸ¹ (p âŸ¹ r)) ), LAâ‚‚, [:p, :q, :r]),
    Sequent(S, :((p âŸ¹ q) âŸ¹ (p âŸ¹ r)), MP, [(1, 2)]),
    Sequent(S, :(p âŸ¹ r), MP, [(1, 5)])
    ]

    verify(proof)
end

#Verification of the proof of :((s âŸ¹ Â¬Â¬s))
let S = Expr[]
    proof::FormalProof = [
    Sequent(S, :(Â¬(Â¬(Â¬s)) âŸ¹ Â¬s), LAâ‚ƒ, [:(Â¬s)]),
    Sequent(S, :( (Â¬(Â¬(Â¬s)) âŸ¹ Â¬s) âŸ¹ (s âŸ¹ Â¬(Â¬s))), LAâ‚„, [:(Â¬(Â¬s)), :(s)]),
    Sequent(S, :(s âŸ¹ Â¬(Â¬s)), MP, [(1, 2)])
    ]

    verify(proof)
end

"""
Recurses through the expression `expr`, searching for an occurrence of the expression `pf` on the right of an implication sign.
The output is a vector of terms that were on the left side of an implication sign.
"""
function nested(expr::WFF, pf::WFF)
    if expr == pf
        return [pf]
    end

    if typeof(expr) != Symbol && expr.args[begin] == :âŸ¹
        a, b = expr.args[end-1:end]

        return [a; nested(b, pf)]
    end
    return expr
end

"""
Returns a set of tautologies obtained by plugging the elements of the input set `S` into to the logical axioms.
A dictionary is used to store information regarding which logical axiom and inputs were used to produce a particular expression.
"""
function Ï„(S::Vector{Any})
    T, D = Expr[], Dict{Expr, Tuple{Function, Vector{Any}}}()

    for a âˆˆ S
        push!(T, LAâ‚ƒ(a)); push!(D, LAâ‚ƒ(a)=>(LAâ‚ƒ, [a]))
        for b âˆˆ S
            push!(T, LAâ‚�(a, b)); push!(D, LAâ‚�(a, b)=>(LAâ‚�, [a, b]))
            push!(T, LAâ‚„(a, b)); push!(D, LAâ‚„(a, b)=>(LAâ‚„, [a, b]))
            for c âˆˆ S
                push!(T, LAâ‚‚(a, b, c)); push!(D, LAâ‚‚(a, b, c)=>(LAâ‚‚, [a, b, c]))
            end
        end
    end

    (T, D)
end

"""
Produces new propositional formulae from the given set `S` by recursively joining existing propositions with `Â¬` and `âŸ¹`.
"""
function Rð�•ƒ(S, n::Int)
    n == 0 ?
    S :
    [
    Rð�•ƒ(S, n-1);
    [:(Â¬$s) for s âˆˆ Rð�•ƒ(S, n-1)];
    [:($sâŸ¹$t) for s âˆˆ Rð�•ƒ(S, n-1) for t âˆˆ Rð�•ƒ(S, n-1)]
    ] |> Set |> collect
end

ð�•ƒ = (:s,)

"""
Returns a formal proof (list of sequents) of a propositional formula `prop`, using tautologies specified in the list `T`.

The output is an empty list is no proof can be produced using the tautologies in `T`.
"""
function proof_of(prop::WFF, T, D; avoid = [])
    formalproof = Sequent[]

    function explore()
        â„’ = [(nested(t, prop), t) for t âˆˆ T]
        filter!(V -> V[1][end] == prop, â„’)

        for l âˆˆ â„’
            push!(formalproof, Sequent(Expr[], l[end], D[l[end]]...))
            seqprove(l...)

            length(formalproof) == 0 ? continue : break
        end

        return formalproof
    end

    function seqprove(props, expr)
        if length(props) == 1
            return
        end

        if (props[begin] âˆˆ avoid) || length(Î½(props[begin])) < 2^length(ð�•ƒ)
            formalproof = Sequent[]
            return
        end

        pr = proof_of(props[begin], T, D; avoid = [[props[end]]; avoid])

        if length(pr) == 0
            formalproof = Sequent[]
            return
        end

        append!(formalproof, pr)
        push!(formalproof, Sequent(Expr[], expr.args[end], MP, [(1, 1+length(pr))]))
        seqprove(props[begin+1:end], expr.args[end])
    end

    explore()
end

proof_of(prop::WFF, (S, n)) = proof_of(prop, Ï„(Rð�•ƒ(S, n))...)


#Examples of proofs
proof_of(:(s âŸ¹ Â¬Â¬s), ([:s], 2))

proof_of(:(s âŸ¹ s), ([:s], 1))

proof_of(:((s âŸ¹ Â¬s) âŸ¹ Â¬s), ([:s], 1))  #this has no proofs involving the given tautologies