package proglang

fun factorial(int: Int): Int {
    var cnt = int
    var result = 1;
    while (cnt != 0) result *= (cnt--)
    return result
}

interface IntExpr {
    override fun toString(): String
    class Add(val lhs: IntExpr, val rhs: IntExpr) : IntExpr {
        override fun toString() = "$lhs + $rhs"
    }

    class Sub(val lhs: IntExpr, val rhs: IntExpr) : IntExpr {
        override fun toString() = "$lhs - $rhs"
    }

    class Literal(val value: Int) : IntExpr {
        override fun toString() = "$value"
    }

    class Var(val name: String) : IntExpr {
        override fun toString() = name
    }

    class Mul(val lhs: IntExpr, val rhs: IntExpr) : IntExpr {
        override fun toString() = "$lhs * $rhs"
    }

    class Div(val lhs: IntExpr, val rhs: IntExpr) : IntExpr {
        override fun toString() = "$lhs / $rhs"
    }

    class Fact(val expr: IntExpr) : IntExpr {
        override fun toString() = "$expr!"
    }

    class Paren(val expr: IntExpr) : IntExpr {
        override fun toString() = "($expr)"
    }
}

fun IntExpr.eval(store: Map<String, Int>): Int = when (this) {
    is IntExpr.Add -> lhs.eval(store) + rhs.eval(store)
    is IntExpr.Sub -> lhs.eval(store) - rhs.eval(store)
    is IntExpr.Literal -> value
    is IntExpr.Var -> store[name] ?: throw UndefinedBehaviourException("Variable $name is not defined.")
    is IntExpr.Mul -> lhs.eval(store) * rhs.eval(store)
    is IntExpr.Div -> rhs.eval(store).let { quotient ->
        if (quotient != 0) lhs.eval(store) / quotient
        else throw UndefinedBehaviourException("Attempt to divide by zero.")
    }
    is IntExpr.Fact -> expr.eval(store).let {
        if (it >= 0) factorial(it)
        else throw UndefinedBehaviourException("Attempt to get factorial of a negative.")
    }
    is IntExpr.Paren -> expr.eval(store)
    else -> throw UnsupportedOperationException("The above should account for all kinds of IntExpr.")
}
