module ExpressionProcessing

export expr_to_postfix, is_binary_operator
export PostfixType
export Operator, ADD, SUBTRACT, MULTIPLY, DIVIDE, POWER, ABS, LOG, EXP, SQRT, INV
export ElementType, EMPTY, FLOAT32, OPERATOR, VARIABLE, PARAMETER
export ExpressionElement

@enum Operator ADD=1 SUBTRACT=2 MULTIPLY=3 DIVIDE=4 POWER=5 ABS=6 LOG=7 EXP=8 SQRT=9 INV=10
@enum ElementType EMPTY=0 FLOAT32=1 OPERATOR=2 VARIABLE=3 PARAMETER=4

const binary_operators = [ADD, SUBTRACT, MULTIPLY, DIVIDE, POWER]
const unary_operators = [ABS, LOG, EXP, SQRT]

struct ExpressionElement
	Type::ElementType
	Value::UInt32 # Reinterpret the stored value to type "ElementType" when using it
end

const PostfixType = Vector{ExpressionElement}

"
Converts a julia expression to its postfix notation.
NOTE: All 64-Bit values will be converted to 32-Bit. Be aware of the lost precision.
NOTE: This function is not thread save, especially cache access is not thread save
"
function expr_to_postfix(expression::Expr)::PostfixType
	expr = expression
	if expression.head === :->
		# if typeof(expression.args[2]) == Float64
		# 	println()
		# 	println("Expression: $expression")
		# 	println("Expr: $expr")
		# 	println()
		# 	dump(expression; maxdepth=10)
		# end
		# if the expression equals (x, p) -> (...) then the below statement extracts the expression to evaluate
		if typeof(expression.args[2]) == Float64
			return [convert_to_ExpressionElement(expression.args[2])]
		elseif expression.args[2].head == :block # expressions that are not generated with the parser (./test/parser.jl) contain this extra "block" node, which needs to be skipped
			expr = expression.args[2].args[2] 
		else # ... if the are generated with the parser, this node is not present and therefore doesn't need to be skipped
			expr = expression.args[2]
		end
	end

	# if haskey(cache, expr)
	# 	return cache[expr]
	# end

	postfix = PostfixType()	

	# Special handling in the case where the expression is an array access
	# This can happen if the token is a variable/parameter of the form x[n]/p[n]
	if expr.head == :ref
		exprElement = convert_to_ExpressionElement(expr.args[1], expr.args[2]) # we assume that an array access never contains an expression, as this would not make much sense in this case
		push!(postfix, exprElement)
		# cache[expr] = postfix
		return postfix
	end

	@inbounds operator = get_operator(expr.args[1])

	@inbounds for j in 2:length(expr.args)
		arg = expr.args[j]

		if typeof(arg) === Expr
			append!(postfix, expr_to_postfix(arg))
		elseif typeof(arg) === Symbol # variables/parameters of the form xn/pn
			exprElement = convert_to_ExpressionElement(arg)
			push!(postfix, exprElement)
		else
			exprElement = convert_to_ExpressionElement(convert(Float32, arg))
			push!(postfix, exprElement)
		end

		# only add operator if at least 2 values are added. Needed because e.g. multiple consecutive additions are one subtree with one operator, but multiple operators need to be added to the postfix notation. 
		# For the case where another expression has already been added to the final postfix notation, we check if we are at the first iteration or not ( j != 2)
		if length(postfix) >= 2 && j != 2
			exprElement = convert_to_ExpressionElement(operator)
			push!(postfix, exprElement)
		end
	end

	# For the case this expression has an operator that only takes in a single value like "abs(x)"
	if operator in unary_operators 
		push!(postfix, convert_to_ExpressionElement(operator))
	end

	# cache[expr] = postfix
	return postfix
end

function get_operator(op::Symbol)::Operator
	if op == :+
		return ADD
	elseif op == :-
		return SUBTRACT
	elseif op == :*
		return MULTIPLY
	elseif op == :/
		return DIVIDE
	elseif op == :^
		return POWER
	elseif op == :powabs
		return POWER # TODO: Fix this
	elseif op == :abs
		return ABS
	elseif op == :log
		return LOG
	elseif op == :exp
		return EXP
	elseif op == :sqrt
		return SQRT
	elseif op == :inv
		return INV
	else
		throw("Operator unknown. Operator was $op")
	end
end

"parses a symbol to be either a variable or a parameter and returns the corresponding Expressionelement"
function convert_to_ExpressionElement(element::Symbol)::ExpressionElement
	varStr = String(element)
	index = parse(UInt32, SubString(varStr, 2))

	if varStr[1] == 'x'
		return ExpressionElement(VARIABLE, index)
	elseif varStr[1] == 'p'
		return ExpressionElement(PARAMETER, index)
	else
		throw("Cannot parse symbol to be either a variable or a parameter. Symbol was '$varStr'")
	end
end
"parses a symbol to be either a variable or a parameter and returns the corresponding Expressionelement"
function convert_to_ExpressionElement(element::Symbol, index::Integer)::ExpressionElement
	if element == :x
		return ExpressionElement(VARIABLE, convert(UInt32, index))
	elseif element == :p
		return ExpressionElement(PARAMETER, convert(UInt32, index))
	else
		throw("Cannot parse symbol to be either a variable or a parameter. Symbol was '$varStr'")
	end
end

function convert_to_ExpressionElement(element::Float32)::ExpressionElement
	value = reinterpret(UInt32, element)
	return ExpressionElement(FLOAT32, value)
end
function convert_to_ExpressionElement(element::Float64)::ExpressionElement
	value = reinterpret(UInt32, convert(Float32, element))
	return ExpressionElement(FLOAT32, value)
end
function convert_to_ExpressionElement(element::Operator)::ExpressionElement
	value = reinterpret(UInt32, element)
	return ExpressionElement(OPERATOR, value)
end

function is_binary_operator(operator::Operator)::Bool
	if operator == ADD
		return true
	elseif operator == SUBTRACT
		return true
	elseif operator == MULTIPLY
		return true
	elseif operator == DIVIDE
		return true
	elseif operator == POWER
		return true
	elseif operator == ABS
		return false
	elseif operator == LOG
		return false
	elseif operator == EXP
		return false
	elseif operator == SQRT
		return false
	else
		throw(ArgumentError("Unknown operator '$operator'. Cannot determine if it is binary or not."))
	end
end

end