#pragma once

#include "tinyxml2.h"

#include <phosphor-logging/elog-errors.hpp>
#include <phosphor-logging/log.hpp>
#include <types.hpp>

#include <map>
#include <sstream>
#include <stack>
#include <string>
#include <variant>
#include <vector>

namespace bios
{
/* Can hold one 'option'
 * For example
 *  <option text="TIS" value="0x0"/>
 */
using OptionType = std::tuple<std::string, ipmi::DbusVariant, std::string>;

/* Can hold one 'options'
 * For example
 *  <options>
 *		<option text="TIS" value="0x0"/>
 *		<option text="PTP FIFO" value="0x1"/>
 *		<option text="PTP CRB" value="0x2"/>
 *	</options>
 */
using OptionTypeVector = std::vector<OptionType>;

/* Can hold one 'knob'
 * For example
 *  <knob  type="scalar" setupType="oneof" name="TpmDeviceInterfaceAttempt"
 *  varstoreIndex="14" prompt="Attempt PTP TPM Device Interface"
 *  description="Attempt PTP TPM Device Interface: PTP FIFO, PTP CRB" size="1"
 *  offset="0x0005" depex="Sif( _LIST_ TpmDevice _EQU_ 0 1 ) _AND_ Sif(
 *  TpmDeviceInterfacePtpFifoSupported _EQU_ 0 OR
 *  TpmDeviceInterfacePtpCrbSupported _EQU_ 0 )" default="0x00"
 *CurrentVal="0x00"> <options> <option text="TIS" value="0x0"/> <option
 *text="PTP FIFO" value="0x1"/> <option text="PTP CRB" value="0x2"/>
 *		</options>
 *	</knob>
 */
using BiosBaseTableTypeEntry =
    std::tuple<std::string, bool, std::string, std::string, std::string,
               ipmi::DbusVariant, ipmi::DbusVariant, OptionTypeVector>;

/* Can hold one 'biosknobs'
 * biosknobs has array of 'knob' */
using BiosBaseTableType = std::map<std::string, BiosBaseTableTypeEntry>;

namespace knob
{
/* These are the operators we support in a 'depex' expression
 * Note: We also support '_LIST_', 'Sif', 'Gif', 'Dif', and 'NOT'. But they are
 * handeled sepeartely. */
enum class DepexOperators
{
    unknown = 0,
    OR,
    AND,
    GT,
    GTE,
    LTE,
    LT,
    EQU,
    NEQ,
    MODULO
};

namespace option
{
/* Can hold one 'option' */
struct option
{
    option(std::string text, std::string value) :
        text(std::move(text)), value(std::move(value))
    {}

    std::string text;
    std::string value;
};
} // namespace option

/* Can hold one 'knob' */
struct knob
{
    knob(std::string nameStr, std::string currentValStr, int currentVal,
         std::string descriptionStr, std::string defaultStr,
         std::string promptStr, std::string depexStr,
         std::string& setupTypeStr) :
        depex(false), readOnly(("ReadOnly" == setupTypeStr) ? true : false),
        currentVal(currentVal), nameStr(std::move(nameStr)),
        currentValStr(std::move(currentValStr)),
        descriptionStr(std::move(descriptionStr)),
        defaultStr(std::move(defaultStr)), promptStr(std::move(promptStr)),
        depexStr(std::move(depexStr))
    {}

    bool depex;
    bool readOnly;
    int currentVal;

    std::string nameStr;
    std::string currentValStr;
    std::string descriptionStr;
    std::string defaultStr;
    std::string promptStr;
    std::string depexStr;

    /* Can hold one 'options' */
    std::vector<option::option> options;
};
} // namespace knob

/* Class capable of computing 'depex' expression. */
class Depex
{
  public:
    Depex(std::vector<knob::knob>& knobs) : mKnobs(knobs) {}

    /* Compute 'depex' expression of all knobs in 'biosknobs'. */
    void compute()
    {
        mError.clear();

        for (auto& knob : mKnobs)
        {
            /* if 'depex' == "TRUE" no need to execute expression. */
            if ("TRUE" == knob.depexStr)
            {
                knob.depex = true;
            }
            else if (!knob.readOnly)
            {
                int value = 0;

                if (!evaluateExpression(knob.depexStr, value))
                {
                    mError.emplace_back("bad depex: " + knob.depexStr +
                                        " in knob: " + knob.nameStr);
                }
                else
                {
                    if (value)
                    {
                        knob.depex = true;
                    }
                }
            }
        }
    }

    /* Returns the number of 'knob's which have a bad 'depex' expression. */
    size_t getErrorCount()
    {
        return mError.size();
    }

    /* Prints all the 'knob's which have a bad 'depex' expression. */
    void printError()
    {
        for (auto& error : mError)
        {
            phosphor::logging::log<phosphor::logging::level::ERR>(
                error.c_str());
        }
    }

  private:
    /* Returns 'true' if the argument string is a number. */
    bool isNumber(const std::string& s)
    {
        return !s.empty() &&
               std::find_if(s.begin(), s.end(), [](unsigned char c) {
                   return !std::isdigit(c);
               }) == s.end();
    }

    /* Returns 'true' if the argument string is hex representation of a number.
     */
    bool isHexNotation(const std::string& s)
    {
        return s.compare(0, 2, "0x") == 0 && s.size() > 2 &&
               s.find_first_not_of("0123456789abcdefABCDEF", 2) ==
                   std::string::npos;
    }

    /* Function to find current value of a 'knob'
     * search is done using 'knob' attribute 'name' */
    bool getValue(std::string& variableName, int& value)
    {
        for (auto& knob : mKnobs)
        {
            if (knob.nameStr == variableName)
            {
                value = knob.currentVal;
                return true;
            }
        }

        std::string error =
            "Unable to find knob: " + variableName + " in knob list\n";
        phosphor::logging::log<phosphor::logging::level::ERR>(error.c_str());

        return false;
    }

    /* Get the expression enclosed within brackets, i.e., between '(' and ')' */
    bool getSubExpression(const std::string& expression,
                          std::string& subExpression, size_t& i)
    {
        int level = 1;
        subExpression.clear();

        for (; i < expression.length(); i++)
        {
            if (expression[i] == '(')
            {
                ++level;
            }
            else if (expression[i] == ')')
            {
                --level;
                if (level == 0)
                {
                    break;
                }
            }

            subExpression.push_back(expression[i]);
        }

        if (!subExpression.empty())
        {
            return true;
        }

        return false;
    }

    /* Function to handle operator '_LIST_'
     * Convert a '_LIST_' expression to a normal expression
     * Example "_LIST_ VariableA _EQU_ 0 1" is converted to "VariableA _EQU_ 0
     * OR VariableA _EQU_ 1" */
    bool getListExpression(const std::string& expression,
                           std::string& subExpression, size_t& i)
    {
        subExpression.clear();

        int cnt = 0;
        std::string variableStr;
        std::string operatorStr;

        for (; i < expression.length(); i++)
        {
            if (expression[i] == '(')
            {
                return false;
            }
            else if (expression[i] == ')')
            {
                break;
            }
            else if (expression[i] == ' ')
            {
                /* whitespace */
                continue;
            }
            else
            {
                std::string word;

                /* Get the next word in expression string */
                while ((i < expression.length()) && (expression[i] != ' '))
                {
                    word.push_back(expression[i++]);
                }

                if (word == "_OR_" || word == "OR" || word == "_AND_" ||
                    word == "AND" || word == "NOT")
                {
                    i = i - word.length();
                    break;
                }

                ++cnt;

                if (cnt == 1)
                {
                    variableStr = word;
                }
                else if (cnt == 2)
                {
                    operatorStr = word;
                }
                else
                {
                    if (cnt > 3)
                    {
                        if (operatorStr == "_EQU_" || operatorStr == "EQU")
                        {
                            subExpression += " OR ";
                        }
                        if (operatorStr == "_NEQ_" || operatorStr == "NEQ")
                        {
                            subExpression += " AND ";
                        }
                    }

                    subExpression += "( ";
                    subExpression += variableStr;
                    subExpression += " ";
                    subExpression += operatorStr;
                    subExpression += " ";
                    subExpression += word;
                    subExpression += " )";
                }
            }
        }

        if (!subExpression.empty())
        {
            return true;
        }

        return false;
    }

    /* Function to handle operator 'NOT'
     * 1) Find the variable
     * 2) apply NOT on the variable */
    bool getNotValue(const std::string& expression, size_t& i, int& value)
    {
        std::string word;

        for (; i < expression.length(); i++)
        {
            if (expression[i] == ' ')
            {
                /* whitespace */
                continue;
            }
            else
            {
                /* Get the next word in expression string */
                while ((i < expression.length()) && (expression[i] != ' '))
                {
                    word.push_back(expression[i++]);
                }

                break;
            }
        }

        if (!word.empty())
        {
            if (getValue(word, value))
            {
                value = !value;
                return true;
            }
        }

        return false;
    }

    /* 1) Pop one operator from operator stack, example 'OR'
     * 2) Pop two variable from variable stack, example VarA and VarB
     * 3) Push back result of 'VarA OR VarB' to variable stack
     * 4) Repeat till operator stack is empty
     *
     * The last variable in variable stack is the output of the expression. */
    bool evaluateExprStack(std::stack<int>& values,
                           std::stack<knob::DepexOperators>& operators,
                           int& output)
    {
        if (values.size() != (operators.size() + 1))
        {
            return false;
        }

        while (!operators.empty())
        {
            int b = values.top();
            values.pop();

            int a = values.top();
            values.pop();

            switch (operators.top())
            {
                case knob::DepexOperators::OR:
                    values.emplace(a | b);
                    break;

                case knob::DepexOperators::AND:
                    values.emplace(a & b);
                    break;

                case knob::DepexOperators::EQU:
                    if (a == b)
                    {
                        values.emplace(1);
                        break;
                    }

                    values.emplace(0);
                    break;

                case knob::DepexOperators::NEQ:
                    if (a != b)
                    {
                        values.emplace(1);
                        break;
                    }

                    values.emplace(0);
                    break;

                case knob::DepexOperators::LTE:
                    if (a <= b)
                    {
                        values.emplace(1);
                        break;
                    }

                    values.emplace(0);
                    break;

                case knob::DepexOperators::LT:
                    if (a < b)
                    {
                        values.emplace(1);
                        break;
                    }

                    values.emplace(0);
                    break;

                case knob::DepexOperators::GTE:
                    if (a >= b)
                    {
                        values.emplace(1);
                        break;
                    }

                    values.emplace(0);
                    break;

                case knob::DepexOperators::GT:
                    if (a > b)
                    {
                        values.emplace(1);
                        break;
                    }

                    values.emplace(0);
                    break;

                case knob::DepexOperators::MODULO:
                    if (b == 0)
                    {
                        return false;
                    }
                    values.emplace(a % b);
                    break;

                default:
                    return false;
            }

            operators.pop();
        }

        if (values.size() == 1)
        {
            output = values.top();
            values.pop();

            return true;
        }

        return false;
    }

    /* Evaluvate one 'depex' expression
     * 1) Find a word in expression string
     * 2) If word is a variable push to variable stack
     * 3) If word is a operator push to operator stack
     *
     * Execute the stack at end to get the result of expression. */
    bool evaluateExpression(const std::string& expression, int& output)
    {
        if (expression.empty())
        {
            return false;
        }

        size_t i;
        int value;
        bool ifFormSetOperator = false;
        std::stack<int> values;
        std::stack<knob::DepexOperators> operators;
        std::string subExpression;

        for (i = 0; i < expression.length(); i++)
        {
            if (expression[i] == ' ')
            {
                /* whitespace */
                continue;
            }
            else
            {
                std::string word;

                /* Get the next word in expression string */
                while ((i < expression.length()) && (expression[i] != ' '))
                {
                    word.push_back(expression[i++]);
                }

                if (word == "_OR_" || word == "OR")
                {
                    /* OR and AND has more precedence than other operators
                     * To handle statements like "a != b or c != d"
                     * we need to execute, for above example, both '!=' before
                     * 'or' */
                    if (!operators.empty())
                    {
                        if (!evaluateExprStack(values, operators, value))
                        {
                            return false;
                        }

                        values.emplace(value);
                    }

                    operators.emplace(knob::DepexOperators::OR);
                }
                else if (word == "_AND_" || word == "AND")
                {
                    /* OR and AND has more precedence than other operators
                     * To handle statements like "a == b and c == d"
                     * we need to execute, for above example, both '==' before
                     * 'and' */
                    if (!operators.empty())
                    {
                        if (!evaluateExprStack(values, operators, value))
                        {
                            return false;
                        }

                        values.emplace(value);
                    }

                    operators.emplace(knob::DepexOperators::AND);
                }
                else if (word == "_LTE_")
                {
                    operators.emplace(knob::DepexOperators::LTE);
                }
                else if (word == "_LT_")
                {
                    operators.emplace(knob::DepexOperators::LT);
                }
                else if (word == "_GTE_")
                {
                    operators.emplace(knob::DepexOperators::GTE);
                }
                else if (word == "_GT_")
                {
                    operators.emplace(knob::DepexOperators::GT);
                }
                else if (word == "_NEQ_")
                {
                    operators.emplace(knob::DepexOperators::NEQ);
                }
                else if (word == "_EQU_")
                {
                    operators.emplace(knob::DepexOperators::EQU);
                }
                else if (word == "%")
                {
                    operators.emplace(knob::DepexOperators::MODULO);
                }
                else
                {
                    /* Handle 'Sif(', 'Gif(', 'Dif(' and '('
                     * by taking the inner/sub expression and evaluating it */
                    if (word.back() == '(')
                    {
                        if (word == "Sif(" || word == "Gif(" || word == "Dif(")
                        {
                            ifFormSetOperator = true;
                        }
                        if (!getSubExpression(expression, subExpression, i))
                            break;

                        if (!evaluateExpression(subExpression, value))
                            break;
                    }
                    else if (word == "_LIST_")
                    {
                        if (!getListExpression(expression, subExpression, i))
                            break;

                        --i;

                        if (!evaluateExpression(subExpression, value))
                            break;
                    }
                    else if (word == "NOT")
                    {
                        if (!getNotValue(expression, i, value))
                            break;
                    }
                    else if (isNumber(word) || isHexNotation(word))
                    {
                        try
                        {
                            value = std::stoi(word, nullptr, 0);
                        }
                        catch (const std::exception& ex)
                        {
                            phosphor::logging::log<
                                phosphor::logging::level::ERR>(ex.what());
                            return false;
                        }
                    }
                    else
                    {
                        if (!getValue(word, value))
                            break;
                    }

                    /* 'Sif(', 'Gif(', 'Dif( == IF NOT,
                    we have to negate the vaule */
                    if (ifFormSetOperator == true)
                    {
                        value = !value;
                    }
                    values.emplace(value);
                }
            }
        }

        if (i == expression.length())
        {
            if (evaluateExprStack(values, operators, output))
            {
                return true;
            }
        }

        return false;
    }

  private:
    /* To store all 'knob's in 'biosknobs' */
    std::vector<knob::knob>& mKnobs;

    /* To store all bad 'depex' expression */
    std::vector<std::string> mError;
};

class Xml
{
  public:
    Xml(const char* filePath) : mDepex(std::make_unique<Depex>(mKnobs))
    {
        if (!getKnobs(filePath))
        {
            std::string error =
                "Unable to get knobs in file: " + std::string(filePath);
            throw std::runtime_error(error);
        }
    }

    /* Fill Bios table with all 'knob's which have output of 'depex' expression
     * as 'true' */
    bool getBaseTable(bios::BiosBaseTableType& baseTable)
    {
        baseTable.clear();

        for (auto& knob : mKnobs)
        {
            if (knob.depex)
            {
                std::string text =
                    "xyz.openbmc_project.BIOSConfig.Manager.BoundType.OneOf";
                bios::OptionTypeVector options;

                for (auto& option : knob.options)
                {
                    options.emplace_back(text, option.value, option.text);
                }

                bios::BiosBaseTableTypeEntry baseTableEntry = std::make_tuple(
                    "xyz.openbmc_project.BIOSConfig.Manager.AttributeType."
                    "Enumeration",
                    false, knob.promptStr, knob.descriptionStr, "./",
                    knob.currentValStr, knob.defaultStr, options);

                baseTable.emplace(knob.nameStr, baseTableEntry);
            }
        }

        if (!baseTable.empty())
        {
            return true;
        }

        return false;
    }

    /* Execute all 'depex' expression */
    bool doDepexCompute()
    {
        mDepex->compute();

        if (mDepex->getErrorCount())
        {
            mDepex->printError();
            return false;
        }

        return true;
    }

  private:
    /* Get 'option' */
    void getOption(tinyxml2::XMLElement* pOption)
    {
        if (pOption)
        {
            std::string valueStr;
            std::string textStr;

            if (pOption->Attribute("text"))
                valueStr = pOption->Attribute("text");

            if (pOption->Attribute("value"))
                textStr = pOption->Attribute("value");

            mKnobs.back().options.emplace_back(pOption->Attribute("text"),
                                               pOption->Attribute("value"));
        }
    }

    /* Get 'options' */
    void getOptions(tinyxml2::XMLElement* pKnob)
    {
        uint16_t reserveCnt = 0;

        /* Get node options inside knob */
        tinyxml2::XMLElement* pOptions = pKnob->FirstChildElement("options");

        if (pOptions)
        {
            for (tinyxml2::XMLElement* pOption =
                     pOptions->FirstChildElement("option");
                 pOption; pOption = pOption->NextSiblingElement("option"))
            {
                ++reserveCnt;
            }

            mKnobs.back().options.reserve(reserveCnt);

            /* Loop through all option inside options */
            for (tinyxml2::XMLElement* pOption =
                     pOptions->FirstChildElement("option");
                 pOption; pOption = pOption->NextSiblingElement("option"))
            {
                getOption(pOption);
            }
        }
    }

    /* Get 'knob' */
    void getKnob(tinyxml2::XMLElement* pKnob)
    {
        if (pKnob)
        {
            int currentVal = 0;
            std::string nameStr;
            std::string currentValStr;
            std::string descriptionStr;
            std::string defaultStr;
            std::string depexStr;
            std::string promptStr;
            std::string setupTypeStr;

            if (!pKnob->Attribute("name") || !pKnob->Attribute("CurrentVal"))
            {
                return;
            }

            nameStr = pKnob->Attribute("name");
            currentValStr = pKnob->Attribute("CurrentVal");
            std::stringstream ss;
            ss << std::hex << currentValStr;
            if (ss.good())
            {
                ss >> currentVal;
            }
            else
            {
                std::string error = "Invalid hex value input " + currentValStr +
                                    " for " + nameStr + "\n";
                phosphor::logging::log<phosphor::logging::level::ERR>(
                    error.c_str());
                return;
            }
            if (pKnob->Attribute("description"))
                descriptionStr = pKnob->Attribute("description");

            if (pKnob->Attribute("default"))
                defaultStr = pKnob->Attribute("default");

            if (pKnob->Attribute("depex"))
                depexStr = pKnob->Attribute("depex");

            if (pKnob->Attribute("prompt"))
                promptStr = pKnob->Attribute("prompt");

            if (pKnob->Attribute("setupType"))
                setupTypeStr = pKnob->Attribute("setupType");

            mKnobs.emplace_back(nameStr, currentValStr, currentVal,
                                descriptionStr, defaultStr, promptStr, depexStr,
                                setupTypeStr);

            getOptions(pKnob);
        }
    }

    /* Get 'biosknobs' */
    bool getKnobs(const char* biosXmlFilePath)
    {
        uint16_t reserveCnt = 0;

        mKnobs.clear();

        tinyxml2::XMLDocument biosXml;

        /* Load the XML file into the Doc instance */
        biosXml.LoadFile(biosXmlFilePath);

        /* Get 'SYSTEM' */
        tinyxml2::XMLElement* pRootElement = biosXml.RootElement();
        if (pRootElement)
        {
            /* Get 'biosknobs' inside 'SYSTEM' */
            tinyxml2::XMLElement* pBiosknobs =
                pRootElement->FirstChildElement("biosknobs");
            if (pBiosknobs)
            {
                for (tinyxml2::XMLElement* pKnob =
                         pBiosknobs->FirstChildElement("knob");
                     pKnob; pKnob = pKnob->NextSiblingElement("knob"))
                {
                    ++reserveCnt;
                }

                /* reserve before emplace_back will avoids realloc(s) */
                mKnobs.reserve(reserveCnt);

                for (tinyxml2::XMLElement* pKnob =
                         pBiosknobs->FirstChildElement("knob");
                     pKnob; pKnob = pKnob->NextSiblingElement("knob"))
                {
                    getKnob(pKnob);
                }
            }
        }

        if (!mKnobs.empty())
        {
            return true;
        }

        return false;
    }

  private:
    /* To store all 'knob's in 'biosknobs' */
    std::vector<knob::knob> mKnobs;

    /* Object of Depex class to compute 'depex' expression */
    std::unique_ptr<Depex> mDepex;
};
} // namespace bios