// SPDX-License-Identifier: BSD-3-Clause
package org.islandoftex.arara.model

import org.islandoftex.arara.Arara
import org.islandoftex.arara.configuration.AraraSpec
import org.islandoftex.arara.localization.LanguageController
import org.islandoftex.arara.localization.Messages
import org.islandoftex.arara.ruleset.Conditional
import org.islandoftex.arara.utils.Methods
import org.mvel2.templates.TemplateRuntime

/**
 * Implements the evaluator model, on which a conditional can be analyzed and
 * processed.
 *
 * @author Island of TeX
 * @version 5.0
 * @since 4.0
 */
class Evaluator {
    // this attribute holds the maximum number of
    // loops arara will accept; it's like
    // reaching infinity
    private val loops: Int = Arara.config[AraraSpec.Execution.maxLoops]

    // the counter for the current execution, it
    // helps us keep track of the number of times
    // this evaluation has happened, and also to
    // prevent potential infinite loops
    private var counter: Int = 0

    // a flag that indicates the
    // evaluation to halt regardless
    // of the the result
    private var halt: Boolean = false

    /**
     * Check if a condition is of type if or unless and whether halt
     * is set.
     * @param type The type to check.
     * @param haltCheck The value [halt] should have.
     * @return `(type == if || type == unless) && haltCheck`
     */
    private fun isIfUnlessAndHalt(
        type: Conditional.ConditionalType,
        haltCheck: Boolean = true
    ): Boolean =
            (type == Conditional.ConditionalType.IF ||
                    type == Conditional.ConditionalType.UNLESS) &&
                    halt == haltCheck

    /**
     * Only run the evaluation of the conditional including a check whether
     * the result needs to be inverted.
     * @param conditional The conditional.
     * @return The result of the evaluation.
     */
    @Throws(AraraException::class, RuntimeException::class)
    private fun evaluateCondition(conditional: Conditional): Boolean {
        val result = TemplateRuntime.eval("@{ " + conditional.condition + " }",
                Methods.getConditionalMethods())
        return if (result is Boolean) {
            if (conditional.type == Conditional.ConditionalType.UNLESS ||
                    conditional.type == Conditional.ConditionalType.UNTIL)
                !result
            else
                result
        } else {
            throw AraraException(messages.getMessage(
                    Messages.ERROR_EVALUATE_NOT_BOOLEAN_VALUE))
        }
    }

    /**
     * Evaluate the provided conditional.
     *
     * @param conditional The conditional object.
     * @return A boolean value indicating if the conditional holds.
     * @throws AraraException Something wrong happened, to be caught in the
     * higher levels.
     */
    @Throws(AraraException::class)
    @Suppress("TooGenericExceptionCaught")
    fun evaluate(conditional: Conditional): Boolean {
        // when in dry-run mode or not evaluating a
        // conditional, arara always ignores conditional
        // evaluations
        if (conditional.type == Conditional.ConditionalType.NONE ||
                Arara.config[AraraSpec.Execution.dryrun] ||
                isIfUnlessAndHalt(conditional.type, true))
            return false
        else if (isIfUnlessAndHalt(conditional.type, false)) {
            halt = true
        }

        // check counters and see if the execution
        // has reached our concept of infinity,
        // thus breaking the cycles
        counter++
        return when {
            conditional.type === Conditional.ConditionalType.WHILE
                    && counter > loops -> false
            conditional.type === Conditional.ConditionalType.UNTIL
                    && counter >= loops -> false
            else -> {
                try {
                    evaluateCondition(conditional)
                } catch (exception: RuntimeException) {
                    throw AraraException(messages.getMessage(Messages
                            .ERROR_EVALUATE_COMPILATION_FAILED),
                            exception)
                }
            }
        }
    }

    companion object {
        // the application messages obtained from the
        // language controller
        private val messages = LanguageController
    }
}
