/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

package mozilla.components.lib.jexl.evaluator

import mozilla.components.lib.jexl.ast.AstNode
import mozilla.components.lib.jexl.grammar.Grammar
import mozilla.components.lib.jexl.value.JexlArray
import mozilla.components.lib.jexl.value.JexlBoolean
import mozilla.components.lib.jexl.value.JexlDouble
import mozilla.components.lib.jexl.value.JexlInteger
import mozilla.components.lib.jexl.value.JexlObject
import mozilla.components.lib.jexl.value.JexlString
import mozilla.components.lib.jexl.value.JexlUndefined
import mozilla.components.lib.jexl.value.JexlValue

typealias Transform = (JexlValue, List<JexlValue>) -> JexlValue

internal class EvaluatorException(message: String) : Exception(message)

/**
 * The evaluator takes a JEXL abstract syntax tree as generated by the <code>Parser</code> and calculates its value
 * within a given context.
 */
internal class Evaluator(
    internal val context: JexlContext = JexlContext(),
    internal val grammar: Grammar = Grammar(),
    internal val transforms: Map<String, Transform> = emptyMap(),
    internal val relativeContext: JexlObject = JexlObject(),
) {

    @Throws(EvaluatorException::class)
    fun evaluate(node: AstNode): JexlValue =
        EvaluatorHandlers.evaluateWith(this, node)

    internal fun evaluateArray(nodes: List<AstNode>): List<JexlValue> {
        return nodes.map { evaluate(it) }
    }

    internal fun evaluateObject(properties: Map<String, AstNode>): Map<String, JexlValue> {
        return properties.mapValues { entry ->
            evaluate(entry.value)
        }
    }

    fun filterRelative(subject: JexlValue, expression: AstNode): JexlValue {
        val filterSubject = subject as? JexlArray ?: JexlArray(
            subject,
        )

        val values = filterSubject.value.filter { element ->
            val subContext = element as? JexlObject ?: JexlObject()
            val evaluator = Evaluator(context, grammar, transforms, subContext)
            val value = evaluator.evaluate(expression)

            value.value as Boolean
        }

        return JexlArray(values)
    }

    fun filterStatic(subject: JexlValue, expression: AstNode): JexlValue {
        val result = evaluate(expression)

        return when {
            result is JexlBoolean -> if (result.value) { subject } else {
                JexlUndefined()
            }

            subject is JexlUndefined -> subject

            subject is JexlObject && result is JexlString -> subject.value[result.value]
                ?: JexlUndefined()

            subject is JexlArray && result is JexlInteger -> subject.value.getOrNull(result.value)
                ?: JexlUndefined()

            // We just convert a double to int here .. hoping for the best!
            subject is JexlArray && result is JexlDouble -> subject.value.getOrNull(result.value.toInt())
                ?: JexlUndefined()

            else -> throw EvaluatorException("Cannot filter $subject by $result")
        }
    }
}
