package interpreter

import io.Logger
import prolog.ast.arithmetic.Expression
import prolog.ast.arithmetic.Integer
import prolog.ast.logic.Clause
import prolog.ast.logic.Fact
import prolog.ast.logic.LogicOperand
import prolog.ast.logic.Rule
import prolog.ast.terms.*
import prolog.builtins.*

/**
 * Preprocessor for Prolog
 *
 * This class preprocesses Prolog code and applies various transformations such as recognizing builtins.
 */
open class Preprocessor {
    /**
     * Preprocesses the input Prolog code.
     *
     * @param input The already parsed Prolog code as a list of clauses.
     * @return The preprocessed Prolog code as a list of clauses.
     */
    fun preprocess(input: List<Clause>): List<Clause> {
        return input.map { preprocess(it) }
    }

    fun preprocess(input: Query): Query {
        return Query(preprocess(input.query) as Goal)
    }

    private fun preprocess(clause: Clause): Clause {
        return when (clause) {
            is Fact -> {
                Fact(preprocess(clause.head) as Head)
            }

            is Rule -> {
                Rule(
                    preprocess(clause.head) as Head,
                    preprocess(clause.body as Term) as Body
                )
            }

            else -> clause
        }
    }

    protected open fun preprocess(term: Term, nested: Boolean = false): Term {
        // TODO Remove hardcoding by storing the functors as constants in operators?

        val prepped = when (term) {
            Variable("_") -> AnonymousVariable.create()
            is Atom, is Structure -> {
                // Preprocess the arguments first to recognize builtins
                val args = if (term is Structure) {
                    term.arguments.map { preprocess(it, nested = true) }
                } else emptyList()

                when (term.functor) {
                    // Analysis
                    Functor.of("functor/3") -> FunctorOp(args[0], args[1], args[2])
                    Functor.of("arg/3") -> Arg(args[0], args[1], args[2])
                    Functor.of("clause/2") -> ClauseOp(args[0] as Head, args[1] as Body)
                    Functor.of("=../2") -> Univ(args[0], args[1])
                    Functor.of("numbervars/1") -> NumberVars(args[0])
                    Functor.of("numbervars/3") -> NumberVars(args[0], args[1] as Integer, args[2])

                    // Arithmetic
                    Functor.of("inf/0") -> Integer(Int.MAX_VALUE)
                    Functor.of("=\\=/2") -> if (args.all { it is Expression }) {
                        EvaluatesToDifferent(args[0] as Expression, args[1] as Expression)
                    } else term

                    Functor.of("=:=/2") -> if (args.all { it is Expression }) {
                        EvaluatesTo(args[0] as Expression, args[1] as Expression)
                    } else term

                    Functor.of("is/2") -> if (args.all { it is Expression }) {
                        Is(args[0] as Expression, args[1] as Expression)
                    } else term

                    Functor.of("-/1") -> if (args.all { it is Expression }) Negate(args[0] as Expression) else term
                    Functor.of("+/1") -> if (args.all { it is Expression }) Positive(args[0] as Expression) else term
                    Functor.of("+/2") -> if (args.all { it is Expression }) {
                        Add(args[0] as Expression, args[1] as Expression)
                    } else term

                    Functor.of("-/2") -> if (args.all { it is Expression }) {
                        Subtract(args[0] as Expression, args[1] as Expression)
                    } else term

                    Functor.of("*/2") -> if (args.all { it is Expression }) {
                        Multiply(args[0] as Expression, args[1] as Expression)
                    } else term

                    Functor.of("//2") -> if (args.all { it is Expression }) {
                        Divide(args[0] as Expression, args[1] as Expression)
                    } else term

                    Functor.of("between/3") -> if (args.all { it is Expression }) {
                        Between(args[0] as Expression, args[1] as Expression, args[2] as Expression)
                    } else term

                    Functor.of("succ/2") -> if (args.all { it is Expression }) {
                        Successor(args[0] as Expression, args[1] as Expression)
                    } else term

                    Functor.of("plus/3") -> if (args.all { it is Expression }) {
                        Plus(args[0] as Expression, args[1] as Expression, args[2] as Expression)
                    } else term

                    // Control
                    Functor.of("fail/0") -> Fail
                    Functor.of("false/0") -> False
                    Functor.of("true/0") -> True
                    Functor.of("!/0") -> Cut()
                    Functor.of(",/2") -> Conjunction(args[0] as LogicOperand, args[1] as LogicOperand)
                    Functor.of(";/2") -> Disjunction(args[0] as LogicOperand, args[1] as LogicOperand)
                    Functor.of("\\+/1") -> Not(args[0] as Goal)

                    // Database
                    Functor.of("dynamic/1") -> Dynamic(Functor.of((args[0] as Atom).name))
                    Functor.of("retract/1") -> Retract(args[0])
                    Functor.of("retractall/1") -> RetractAll(args[0])
                    Functor.of("assert/1") -> {
                        if (args[0] is Rule) {
                            Assert(args[0] as Rule)
                        } else {
                            Assert(Fact(args[0] as Head))
                        }
                    }

                    Functor.of("asserta/1") -> {
                        if (args[0] is Rule) {
                            AssertA(args[0] as Rule)
                        } else {
                            AssertA(Fact(args[0] as Head))
                        }
                    }

                    Functor.of("assertz/1") -> {
                        if (args[0] is Rule) {
                            AssertZ(args[0] as Rule)
                        } else {
                            AssertZ(Fact(args[0] as Head))
                        }
                    }

                    // Delimited Continuations
                    Functor.of("reset/3") -> Reset(args[0] as Goal, args[1], args[2])
                    Functor.of("shift/1") -> Shift(args[0])

                    // IO
                    Functor.of("write/1") -> Write(args[0])
                    Functor.of("nl/0") -> Nl
                    Functor.of("writeln/1") -> WriteLn(args[0])
                    Functor.of("read/1") -> Read(args[0])

                    // Lists
                    Functor.of("member/2") -> Member(args[0], args[1])
                    Functor.of("append/3") -> Append(args[0], args[1], args[2])

                    // Loading
                    Functor.of("consult/1") -> Consult(args[0])
                    Functor.of("initialization/1") -> Initialization(args[0] as Goal)

                    // Meta
                    Functor.of("call/1") -> Call(args[0])
                    Functor.of("once/1") -> Once(args[0] as Goal)
                    Functor.of("ignore/1") -> Ignore(args[0] as Goal)

                    // Other
                    Functor.of("forall/2") -> ForAll(args[0] as LogicOperand, args[1] as Goal)

                    // Unification
                    Functor.of("=/2") -> Unify(args[0], args[1])
                    Functor.of("\\=/2") -> NotUnify(args[0], args[1])
                    Functor.of("==/2") -> Equivalent(args[0], args[1])
                    Functor.of("\\==/2") -> NotEquivalent(args[0], args[1])

                    // Verification
                    Functor.of("atomic/1") -> Atomic(args[0])
                    Functor.of("compound/1") -> Compound(args[0])
                    Functor.of("nonvar/1") -> NonVar(args[0])
                    Functor.of("var/1") -> Var(args[0])

                    Functor.of(":-/2") -> Rule(args[0] as Head, args[1] as Body)

                    else -> {
                        if (term is Structure) term.arguments = args
                        term
                    }
                }
            }

            else -> term
        }

        Logger.debug(
            "Preprocessed term $term into $prepped (kind ${prepped::class.simpleName})",
            !nested && (prepped != term || prepped::class != term::class)
        )

        return prepped
    }
}
