package com.pixelized.shared.lwa.parser.expression

import com.pixelized.shared.lwa.parser.dice.DiceParser
import com.pixelized.shared.lwa.parser.word.WordParser
import java.net.URI

/**
 * Highly inspired by the following javascript implementation:
 * https://tiarkrompf.github.io/notes/?/just-write-the-parser/
 */
class ExpressionParser(
    private val diceParser: DiceParser,
    private val wordParser: WordParser,
) {
    companion object {
        private val tokenBreak = arrayOf(
            '+', '-', '/', '*', '(', ')', ','
        )
        private val tokenBreakException = mapOf(
            '/' to listOf("http:", "https:"),
            '.' to listOf("http:", "https:"),
            '+' to listOf("http:", "https:"),
            '-' to listOf("http:", "https:"),
        )
        private val operators = mapOf(
            '+' to Operator(
                evaluations = { first, second -> Expression.Add(first, second) },
                priority = 100,
                association = 1,
            ),
            '-' to Operator(
                evaluations = { first, second -> Expression.Minus(first, second) },
                priority = 100,
                association = 1,
            ),
            '*' to Operator(
                evaluations = { first, second -> Expression.Prod(first, second) },
                priority = 200,
                association = 1,
            ),
            '/' to Operator(
                evaluations = { first, second -> Expression.Div(first, second) },
                priority = 200,
                association = 1,
            ),
        )

        private inline fun guard(condition: Boolean, error: () -> Error) {
            return if (condition.not()) throw error() else Unit
        }
    }

    private val stack = Stack()

    /**
     * Helper method to determined when a token should end.
     * Every characters that are not un the [Companion.tokenBreak] list can be part of a token.
     * @see Companion.tokenBreak
     */
    private fun isToken(
        currentToken: String? = null,
    ): Boolean = stack.peek().let {
        it != null && !tokenBreak.contains(it) ||
                (tokenBreakException[it]?.any { currentToken?.contains(it) ?: false } ?: false)
    }

    /**
     * Helper method to pull from the stack until we have a complete token.
     * Use the isToken() method to determined when a token should end.
     * @see isToken
     */
    private fun token(): String {
        guard(isToken()) {
            Error.ExpectedTokenCharacter(
                actual = stack.peek(),
                expression = stack.input,
            )
        }
        val token = StringBuilder()
        do {
            stack.pull().let(token::append)
        } while (isToken(token.toString()))
        return token.toString()
    }

    /**
     * Helper method to construct an expression from a token.
     * This method handle bracket recursion, functions [min(), max(), etc.]
     * and token expression creation (Word, Dice, Flat, etc.).
     * @see token
     */
    private fun factor(): Expression? {
        when (stack.peek()) {
            '(' -> {
                stack.moveCursor()
                val result = evaluate()
                guard(stack.peek() == ')') {
                    Error.ExpectedOperator(
                        expected = ')',
                        actual = stack.peek(),
                        expression = stack.input
                    )
                }
                stack.moveCursor()
                return result
            }

            '-' -> { // this is considered as a sign function for the following expression.
                stack.moveCursor()
                val result = evaluate()
                return result?.let(Expression::Inversion)
            }

            '+' -> { // this is considered as a sign function for the following expression.
                stack.moveCursor()
                val result = evaluate()
                return result
            }

            else -> when (val token = token()) {
                "min" -> {
                    // consume the '(' character
                    stack.moveCursor()
                    // evaluate the content of the first parameter.
                    val first = evaluate()
                    // check that the expression is well formed, need a ,.
                    guard(stack.peek() == ',') {
                        Error.ExpectedOperator(
                            expected = ',',
                            actual = stack.peek(),
                            expression = stack.input
                        )
                    }
                    // consume the ',' character of the second parameter.
                    stack.moveCursor()
                    // evaluate the content of the second parameter.
                    val second = evaluate()
                    // check that the expression is well formed, need a ).
                    guard(stack.peek() == ')') {
                        Error.ExpectedOperator(
                            expected = ')',
                            actual = stack.peek(),
                            expression = stack.input
                        )
                    }
                    // consume the ')' character
                    stack.moveCursor()
                    // build the final function expression
                    return Expression.Minimum(first, second)
                }

                "max" -> {
                    // consume the '(' character
                    stack.moveCursor()
                    // evaluate the content of the first parameter.
                    val first = evaluate()
                    // check that the expression is well formed, need a ,.
                    guard(stack.peek() == ',') {
                        Error.ExpectedOperator(
                            expected = ',',
                            actual = stack.peek(),
                            expression = stack.input
                        )
                    }
                    // consume the ',' character of the second parameter.
                    stack.moveCursor()
                    // evaluate the content of the second parameter.
                    val second = evaluate()
                    // check that the expression is well formed, need a ).
                    guard(stack.peek() == ')') {
                        Error.ExpectedOperator(
                            expected = ')',
                            actual = stack.peek(),
                            expression = stack.input
                        )
                    }
                    // consume the ')' character
                    stack.moveCursor()
                    // build the final function expression
                    return Expression.Maximum(first, second)
                }

                "floor5" -> {
                    // consume the '(' character
                    stack.moveCursor()
                    // evaluate the content of the first parameter.
                    val expression = evaluate()
                    // check that the expression is well formed, need a ).
                    guard(stack.peek() == ')') {
                        Error.ExpectedOperator(
                            expected = ')',
                            actual = stack.peek(),
                            expression = stack.input
                        )
                    }
                    // consume the ')' character
                    stack.moveCursor()
                    // build the final function expression
                    return Expression.Floor5(expression)
                }

                else -> {
                    val value = token.toIntOrNull()
                    if (value != null) {
                        return Expression.Flat(value)
                    }

                    val word = wordParser.parse(token)
                    if (word != null) {
                        return Expression.WordExpression(word)
                    }

                    val dice = diceParser.parse(token)
                    if (dice != null) {
                        return Expression.DiceExpression(dice)
                    }

                    val url = try {
                        URI.create(token).toString()
                    } catch (_: Exception) {
                        null
                    }
                    if (url != null) {
                        return Expression.UrlExpression(url)
                    }

                    throw Error.UnRecognizedToken(actual = token, expression = stack.input)
                }
            }
        }
    }

    /**
     * Helper method to handle operator priority and associativity.
     * build with the example https://tiarkrompf.github.io/notes/?/just-write-the-parser/ in mind.
     * @param minPriority the minimum priority to check.
     * @return a nullable [Expression]
     */
    private fun evaluate(minPriority: Int): Expression? {
        var res = factor()
        while (
            operators.contains(stack.peek()) &&
            (operators[stack.peek()]?.priority ?: 0) >= minPriority
        ) {
            val nextMin = operators[stack.peek()]?.let { it.priority + it.association } ?: 0
            res = operators[stack.pull()]?.evaluations?.invoke(
                res,
                evaluate(nextMin)
            )
        }
        return res
    }

    /**
     * Helper method to evaluate a expression that have been initialized into the stack.
     * This is typically call just after the stack have been initialized but also when a recursion is needed.
     * @return a nullable [Expression]
     */
    private fun evaluate(): Expression? {
        return evaluate(0)
    }

    /**
     * Public parsing method for an expression.
     * @param input the input expression [String]
     * @return a nullable [Expression]
     */
    fun parse(input: String?): Expression? {
        return input.takeIf { it?.isNotBlank() == true }?.let {
            stack.init(expression = it)
            evaluate()
        }
    }

    sealed class Error(
        message: String,
        expression: String,
    ) : Exception("Expession:$expression - $message") {

        class ExpectedTokenCharacter(
            val actual: Char?,
            val expression: String,
        ) : Error(
            "Expected a token character (either a letter or a digit), but was: $actual", expression
        )

        class ExpectedOperator(
            val expected: Char,
            val actual: Char?,
            val expression: String,
        ) : Error("Expected operator: $expected, but was $actual", expression)

        class UnRecognizedToken(
            val actual: String,
            val expression: String,
        ) : Error(
            "Expected a specific token 'word', 'digit' or 'function', but was: $actual",
            expression
        )
    }

    private data class Operator(
        val evaluations: (Expression?, Expression?) -> Expression?,
        val priority: Int,      // operator priority, higher is priority
        val association: Int,   // 0 : left associativity, 1: right
    )

    private data class Stack(
        var input: String = "",
        private var cursor: Int = 0,
        private var peek: Char? = null,
    ) {
        fun init(expression: String) {
            input = expression
            cursor = 0
            peek = null
            moveCursor()
        }

        fun peek(): Char? = peek

        fun pull(): Char? = peek().also { moveCursor() }

        fun moveCursor() {
            peek = input.getOrNull(cursor)
            cursor = (cursor + 1).takeIf { it < input.length } ?: -1
        }
    }
}