------------------------------------------
-- QueryParser.lua
-- Author: Sorontur @MalygosEU
-- This is based on BossTactics licence
------------------------------------------

local L = LibStub("AceLocale-3.0"):GetLocale("BossTactics")

---------------------------------------------
--codes: AND = 1 OR = 0
--error messages do not work properly
---------------------------------------------

---------------------------------------------------
--state functions declared as locals
---------------------------------------------------

local function MakeError(msg)
	local val = {}
	val.err = true
    local tbl = { strsplit(":", msg) }
    val.msg = strsub(tbl[#tbl],2)
    return val
end

--state where an "and" or an "or" must come
local function State1(query,pos)
	--create lookahead
	local look = strsub(query,pos,pos+1)
	if(look == "or")then
		return 0, pos+2
	elseif(look == "an")then
		if(strbyte(query, pos+2) == strbyte("d"))then
			return 1, pos+3
		end
	end
	error("state1 error "..look)
end;

--state where an "<" ,">", "=", "<=" or an ">=" must come
local function State2(query,pos)
	--create lookahead
	local look = strsub(query,pos,pos)
	if(look == "=")then
		return "=", pos+1
	elseif(look == "<" or look == ">")then
		local look2 = strsub(query,pos+1,pos+1)
		if(look2 == "=")then
			return look..look2, pos+2
		else
			return look,pos+1
		end
	end
	error("state2 error "..look)
end;

--parses a string starting form " to the next " returns the position of the next input char
local function GetString(query,pos)

	local retVal = ""

	if(strbyte(query,pos) == strbyte("\""))then
		pos = pos + 1
		while(strbyte(query,pos) ~= strbyte("\"")) do
			if(pos>strlen(query))then
				error("unclosed String")
				break;
			end
			retVal = retVal..strchar(strbyte(query,pos))
            pos = pos+1
		end
		pos = pos + 1;
	else
		error("unclosed String")
	end

	return retVal,pos
end;

local function isDigit(query,pos)

	local code = strbyte(query,pos)
	if(code and code>= 48 and code <=57)then
		return true
	else
		return false
	end

end

local function ParseInteger(query,pos)

	local itemidStr = ""
	while(isDigit(query,pos))do
		itemidStr = itemidStr..strsub(query,pos,pos)
		pos = pos+1
	end
	return itemidStr,pos
end

--parses after an "and" that the precedence is correct
local function ParseExpression1(query,pos)
	local val = {}
	if(strbyte(query, pos) == strbyte("\""))then
        local isOk, tmpString, newPos1 = pcall(GetString,query,pos)
        if(isOk)then
            if(newPos1 > strlen(query) or strbyte(query, newPos1) == strbyte(")"))then
                return tmpString, newPos1
            end
            local isOk, op, newPos2 = pcall(State1,query,newPos1)
            if(isOk)then
                val = {tmpString,op}
                pos = newPos2
            else
            	val = MakeError(op)
            end
        else
        	val = MakeError(tmpString)
        end
    elseif(strbyte(query, pos) == strbyte("("))then
       local isOk, val1, pos1 = pcall(function(query,pos) return BossTactics:ParseBrackets(query,pos) end,query,pos)
       if(isOk)then
       	val = val1
       	pos = pos1
       else
       		val=MakeError(val1)
       end
    else
        error("expr1 error "..strchar(strbyte(query,pos)))
    end

	return val, pos
end

--parses after an "and" that the precedence is correct
local function ParseItemExpression1(query,pos)
	local val = {}

	if(isDigit(query, pos))then
        val1, pos2 = ParseInteger(query,pos)

		if(pos2 > strlen(query) or strbyte(query, pos2) == strbyte(")"))then
            return MakeError("no expression1"), pos2
        end
        local isOk, op, pos3 = pcall(State2,query,pos2)
        if(isOk)then
        	if(isDigit(query,pos3))then
                local val2, pos4 = ParseInteger(query,pos3)
                if(pos4 > strlen(query) or strbyte(query, pos4) == strbyte(")"))then
                    return {item=val1,comp=op,count=val2}, pos4
                end
                local isOk, op2, pos5 = pcall(State1,query,pos4)
                if(isOk)then
                    val = {{item=val1,comp=op,count=val2},op2}
                    pos = pos5
                else
                    val = MakeError(op)
                end
            else
                val = MakeError("no integer after comparison")
            end
        else
            val = MakeError(op)
        end

    elseif(strbyte(query, pos) == strbyte("("))then
       local isOk, val1, pos1 = pcall(function(query,pos) return BossTactics:ParseItemBrackets(query,pos) end,query,pos)
       if(isOk)then
       	val = val1
       	pos = pos1
       else
       		val=MakeError(val1)
       end
    else
        error("expr1 error "..strchar(strbyte(query,pos)))
    end

	return val, pos
end

local function ParsePrecedence(query,pos,val1,op,AndFun,ExprFun)

	local val = {}
	if(op == 1)then
    	--local isOk, tmpVal, newPos3 = pcall(BossTactics.ParseExpression1,query,pos)
    	local isOk,val2,pos2 = pcall(AndFun,query,pos)
    	if(isOk)then
    		pos = pos2
    	else
    		val2 = MakeError(val2)
    	end

		--put pieces together
		if(pos > strlen(query) or strbyte(query, pos) == strbyte(")"))then
            val.op1 = val1
            val.op = op
            val.op2 = val2
        else
            val.op1 = {
                op1 = val1,
                op = op,
                op2 = val2[1],
            }
            local tmpVal2, newPos4 = ExprFun(query,pos)
            val.op = val2[2]
            val.op2 = tmpVal2
            pos = newPos4
        end
    elseif(op == 0)then
        local tmpVal, newPos3 = ExprFun(query,pos)
        val.op1 = val1
        val.op = op
        val.op2 = tmpVal
        pos = newPos3
    end
    return val,pos
end

--parses Brackets starting with '('
function BossTactics:ParseBrackets(query,pos)
	local val = {}
	local tmpVal, newPos = BossTactics:ParseExpression(query,pos+1)
    if(strbyte(query, newPos) ~= strbyte(")"))then
       error("expr ) error")
   	else
        --if we are at the end of input
        if(newPos+1 > strlen(query) or strbyte(query, newPos+1) == strbyte(")"))then
        	val = tmpVal
    		pos = newPos+1
        else
        	local isOk, op, newPos2 = pcall(State1,query,newPos+1)
        	if(isOk)then
        		local val2, pos3 = ParsePrecedence(query,newPos2,tmpVal,op,ParseExpression1,
        								function(query,pos) return BossTactics:ParseExpression(query,pos) end)
                val = val2
                pos = pos3
            else
            	val = MakeError(op)
            	pos = newPos+1
            end
    	end
   	end

   	return val, pos
end;

--parses Brackets starting with '('
function BossTactics:ParseItemBrackets(query,pos)
	local val = {}
	local tmpVal, newPos = BossTactics:ParseItemExpression(query,pos+1)
    if(strbyte(query, newPos) ~= strbyte(")"))then
       error("expr ) error")
   	else
        --if we are at the end of input
        if(newPos+1 > strlen(query) or strbyte(query, newPos+1) == strbyte(")"))then
        	val = tmpVal
    		pos = newPos+1
        else
        	local isOk, op, newPos2 = pcall(State1,query,newPos+1)
        	if(isOk)then
                local val2, pos3 = ParsePrecedence(query,newPos2,tmpVal,op,ParseItemExpression1,
        								function(query,pos) return BossTactics:ParseItemExpression(query,pos) end)
                val = val2
                pos = pos3
            else
            	val = MakeError(op)
            	pos = newPos+1
            end
    	end
   	end

   	return val, pos
end;


--global function but not for usage! use ParseQuery because it is the wrapper
function BossTactics:ParseItemExpression(query,pos)
	local val = {}

	if(isDigit(query, pos))then
		local val1, pos1 = ParseInteger(query,pos)
       	--if we are at the end of input or a closing bracket, stop recursion
        if(pos1 > strlen(query) or strbyte(query, pos1) == strbyte(")"))then
            return MakeError("no expression"), pos1
        end

        local isOk, op, pos2 = pcall(State2,query,pos1)
        if(isOk)then
        	if(isDigit(query,pos2))then
                local val2, pos3 = ParseInteger(query,pos2)
                 if(pos3 > strlen(query) or strbyte(query, pos3) == strbyte(")"))then
                 	val.op1 = {
                 		item = val1,
                 		comp = op,
                 		count = val2,
                 	}
                    pos = pos3
                 else
                     local isOk, op2, pos4 = pcall(State1,query,pos3)
                     if(isOk)then
                     	local val3, pos5 = ParsePrecedence(query,pos4,{item=val1,comp=op,count=val2},op2,ParseItemExpression1,
        									function(query,pos) return BossTactics:ParseItemExpression(query,pos) end)
                        val = val3
                        pos = pos5
                     else
                        return MakeError(op2),pos4
                     end
                end
            else
                return MakeError("integer expected "..strchar(strbyte(query,pos2))),pos2
            end
        end
	 elseif(strbyte(query, pos) == strbyte("("))then
       local isOk, val1, pos1 = pcall(function(query,pos) return BossTactics:ParseItemBrackets(query,pos) end,query,pos)
       if(isOk)then
       	val = val1
       	pos = pos1
       else
       		val=MakeError(val1)
       end
    else
        return MakeError("expr error "..strchar(strbyte(query,pos))),pos
    end

	return val, pos
end;

--global function but not for usage! use ParseQuery because it is the wrapper
function BossTactics:ParseExpression(query,pos)

	local val = {}
	if(strbyte(query, pos) == strbyte("\""))then
        local isOk, tmpString, newPos1 = pcall(GetString,query,pos)

        if(isOk)then
            --if we are at the end of input or a closing bracket, stop recursion
            if(newPos1 > strlen(query) or strbyte(query, newPos1) == strbyte(")"))then
                return tmpString, newPos1
            end

            --normal operator follows
            local isOk, op, newPos2 = pcall(State1,query,newPos1)
            if(isOk)then
                local val2, pos3 = ParsePrecedence(query,newPos2,tmpString,op,ParseExpression1,
        								function(query,pos) return BossTactics:ParseExpression(query,pos) end)
                val = val2
                pos = pos3
           else
            val = MakeError(op)
           end
        else
            val = MakeError(tmpString)
            pos = newPos1
        end
    elseif(strbyte(query, pos) == strbyte("("))then
       local isOk, val1, pos1 = pcall(function(query,pos) return BossTactics:ParseBrackets(query,pos) end,query,pos)
       if(isOk)then
       	val = val1
       	pos = pos1
       else
       		val=MakeError(val1)
       end
    else
        return MakeError("expr error "..strchar(strbyte(query,pos))),pos
    end
	return val, pos

end;

local function TranslateOperatorToString(operator)
	if(operator == 0)then
		return "or"
	elseif(operator == 1)then
		return "and"
	else
		return nil
	end
end;

function BossTactics:PrintTree(tree, queryType)

	if(queryType == "Addons")then
	    if(type(tree) == "table")then
            if(tree.err)then
                return "Error: "..tree.msg
            else
                return "( "..self:PrintTree(tree.op1, queryType).." "..TranslateOperatorToString(tree.op).." "..self:PrintTree(tree.op2, queryType).." )"
            end
        elseif(type(tree) == "string")then
            return tree
        else
            return "unforseen error "..type(tree).." "..tostring(tree)
        end
    elseif(queryType == "Items")then
    	if(type(tree) == "table")then
    	    if(tree.err)then
                 return "Error: "..tree.msg
            elseif(tree.item)then
            	local itemname, itemlink, itemRarity, itemLevel, itemMinLevel, itemType, itemSubType, itemStackCount = GetItemInfo(tree.item)
            	local r,g,b,hex = GetItemQualityColor(itemRarity)
            	local digitString;
            	if(itemStackCount >= 1000)then
            		digitString = "0000"
            	elseif(itemStackCount >= 100)then
            		digitString = "000"
            	elseif(itemStackCount >= 10) then
            		digitString = "00"
            	else
            		digitString = "0"
            	end
            	return hex.."["..itemname.."]|r".." "..tree.comp.." "..tree.count.." ("..digitString..")"
            else
				if(tree.op)then
					return "( "..self:PrintTree(tree.op1, queryType).." "..TranslateOperatorToString(tree.op).." "..self:PrintTree(tree.op2, queryType).." )"
				else
					return self:PrintTree(tree.op1, queryType)
				end
            end
        else
        	return "print error"
        end
    end
end;

local function CleanSpaces(query)

	if(query == nil)then return nil end
	query = strtrim(query)
	local newRet = ""
	local i = 1
	while( strlen(query) >= i) do
		if(strbyte(query,i) == strbyte(" ") or strbyte(query,i) == strbyte("\n"))then
			--do nothing
		elseif(strbyte(query,i) == strbyte("\"")) then
			newRet = newRet..strchar(strbyte(query,i))
			i = i+1
			while(strbyte(query,i) ~= strbyte("\"") and i<=strlen(query)) do
				newRet = newRet..strchar(strbyte(query,i))
				i = i+1
			end
			newRet = newRet..strchar(strbyte(query,i))
		else
			newRet = newRet..strchar(strbyte(query,i))
		end
		i=i+1;
	end
	return newRet
end;

--checks if a tree contains errors or if it is well done
function BossTactics:CheckTree(tree, queryType)

	if(queryType == "Addons")then
	    if(type(tree) == "table")then
            if(tree.err)then
                return false
            else
                return self:CheckTree(tree.op1, queryType) and self:CheckTree(tree.op2, queryType)
            end
        elseif(type(tree) == "string")then
            return true
        else
            return false
        end
    elseif(queryType == "Items")then
    	if(type(tree) == "table")then
    		if(tree.err)then
                return false
            elseif(tree.item)then
            	local itemname, itemlink, itemRarity = GetItemInfo(tree.item)
            	if(itemname)then
            		return true
            	else
            		return false
            	end
            else
            	if(tree.op)then
            		return self:CheckTree(tree.op1, queryType) and self:CheckTree(tree.op2, queryType)
            	else
                	return self:CheckTree(tree.op1, queryType)
                end
            end
        else
        	return false
        end
    end
end;

----------------------------------------------------------------------------------------
--Function to call if you want to parse a query. This is the parse function for usage
--query: String to parse
--returns: a tree
----------------------------------------------------------------------------------------

function BossTactics:ParseQuery(query, queryType)

	query = CleanSpaces(query)
	if(query == nil or strlen(query) == 0)then
		return {
			err = true,
			msg = "No Input",
		}
	end
	if(strfind(query,":"))then
		return {
			err = true,
			msg = "No :",
		}
	end

	if(strfind(query,"}"))then
		return {
			err = true,
			msg = "No }",
		}
	end

	local retVal, retPos

	if(queryType == "Addons")then
		retVal, retPos = self:ParseExpression(query,1)
	elseif(queryType == "Items")then
		retVal, retPos = self:ParseItemExpression(query,1)
	end
	if(self.debug)then
		self:Print(self:PrintTree(retVal,queryType))
	end
	if(retPos ~= nil and retPos<strlen(query))then
		retVal = {
			err = true,
			msg = "Bracket error",
		}
	end
	return retVal

end;

-----------------------------------------------------------------------
--evaluates a tree of a special type
--returns a string separated by # where every part is a result
-- if you follow the tree
-- assertion: expects a tree without error!
-----------------------------------------------------------------------


function BossTactics:EvaluateQuery(tree, queryType)

	if(queryType == "Addons")then
	    if(type(tree) == "table")then
            return self:EvaluateQuery(tree.op1, queryType).."#"..self:EvaluateQuery(tree.op2, queryType)
        elseif(type(tree) == "string")then
        	local name, title, notes, enabled, loadable, reason = GetAddOnInfo(tree)
            if(reason == "MISSING")then
                return "2"
            elseif(reason == "DISABLED")then
                return "1"
            else
                return "0"
            end
        else
            return ""
        end
    elseif(queryType == "Items")then
    	if(type(tree) == "table")then
            if(tree.item)then
            	return tostring(GetItemCount(tree.item,false,true))
            else
				if(tree.op)then
					return self:EvaluateQuery(tree.op1, queryType).."#"..self:EvaluateQuery(tree.op2, queryType)
				else
					return self:EvaluateQuery(tree.op1, queryType)
				end
			end
		else
			return ""
        end
    end

end;

---------------------------------------------------------------------
-- creates a readable string of one result (expected as list of results)
-- position is an optional parameter
-- returns the string, a overall result of the predicate
--  and a position which is only for internal usage
---------------------------------------------------------------------

function BossTactics:CreateEvaluationString(tree, queryType, result, position)

	if(not position)then position = 1 end

	if(queryType == "Addons")then
	    if(type(tree) == "table")then
            local part1, pred1, pos1 = self:CreateEvaluationString(tree.op1, queryType,result,position)
            local part2, pred2, pos2 = self:CreateEvaluationString(tree.op2, queryType,result,pos1)
            local predResult = false
            if(tree.op == 1)then
                predResult = pred1 and pred2
            elseif(tree.op == 0)then
                predResult = pred1 or pred2
            end
            return "( "..part1.." "..TranslateOperatorToString(tree.op).." "..part2.." )",predResult,pos2
        elseif(type(tree) == "string")then
        	if(result[position] == "0")then
                return "|cFF00FF00"..tree.."|r", true, position+1
            elseif(result[position] == "1")then
                return "|cFFFFFF00"..tree.."|r", false, position+1
            elseif(result[position] == "2")then
                return "|cFFFF0000"..tree.."|r", false, position+1
            end
        else
            return "error", false, position+1
        end
    else
    	if(type(tree) == "table")then
            if(tree.item)then
            	local count = tonumber(result[position])
            	local res = false
            	if(tree.comp == "=")then
            		res = count == tonumber(tree.count)
            	elseif(tree.comp == "<")then
            		res = count < tonumber(tree.count)
            	elseif(tree.comp == "<=")then
            		res = count <= tonumber(tree.count)
            	elseif(tree.comp == ">")then
            		res = count > tonumber(tree.count)
            	elseif(tree.comp == ">=")then
            		res = count >= tonumber(tree.count)
            	end
            	local color = "|cFFFF0000"
            	if(res)then
            		color = "|cFF00FF00"
            	end
            	local itemname, itemlink, itemRarity = GetItemInfo(tree.item)
            	local r,g,b,hex = GetItemQualityColor(itemRarity)
            	return hex.."["..itemname.."]|r"..color.." "..tree.comp.." "..tree.count.." ("..count..")|r",res,position+1
            else
				if(tree.op)then
					local part1, pred1, pos1 = self:CreateEvaluationString(tree.op1, queryType,result,position)
                    local part2, pred2, pos2 = self:CreateEvaluationString(tree.op2, queryType,result,pos1)
                    local predResult = false
                    if(tree.op == 1)then
                        predResult = pred1 and pred2
                    elseif(tree.op == 0)then
                        predResult = pred1 or pred2
                    end
                    return "( "..part1.." "..TranslateOperatorToString(tree.op).." "..part2.." )",predResult,pos2
				else
					return self:CreateEvaluationString(tree.op1, queryType,result,position)
				end
			end
		else
			return ""
        end
    end

end;