/**
 * Copyright (C) 1997-2015 by Dimitri van Heesch.
 *
 * Permission to use, copy, modify, and distribute this software and its
 * documentation under the terms of the GNU General Public License is hereby
 * granted. No representations are made about the suitability of this software
 * for any purpose. It is provided "as is" without express or implied warranty.
 * See the GNU General Public License for more details.
 *
 * Documents produced by Doxygen are derivative works derived from the
 * input used in their production; they are not affected by this license.
 *
 * C++ Expression parser for ENABLED_SECTIONS in Doxygen
 *
 * Features used:
 *     Operators:
 *         &&    AND operator
 *         ||    OR  operator
 *         !     NOT operator
 */

#include <algorithm>

#include "condparser.h"
#include "config.h"
#include "configimpl.h"
#include "configoptions.h"
#include "message.h"

// declarations
static DString error_str = "doxyconfig_error";
static DString resolveConfig(const DString &fileName,int lineNr, const DString &expr);
static DString getConfig(const DString &fileName,int lineNr, const DString &expr);

/**
 * parses and evaluates the given expression.
 * @returns
 * - On error, an error message is returned.
 * - On success, the result of the expression is either "1" or "0".
 */
bool CondParser::parse(const DString &fileName,int lineNr,const DString &expr)
{
  if (expr.empty()) return false;
  m_tokenType = NOTHING;
  m_expr = resolveConfig(fileName, lineNr, expr);

  // initialize all variables
  m_e = m_expr.data();    // let m_e point to the start of the expression

  bool answer=false;
  getToken();
  if (m_tokenType==DELIMITER && m_token.empty())
  {
    // empty expression: answer==false
  }
  else if (m_err.empty())
  {
    answer = parseLevel1();
  }
  if (!m_err.empty())
  {
    warn(fileName,lineNr,"problem evaluating expression '{}': {}", expr, m_err);
  }
  //printf("expr='%s' answer=%d\n",expr,answer);
  return answer;
}


/**
 * checks if the given char c is a delimiter
 * minus is checked apart, can be unary minus
 */
static bool isDelimiter(const char c)
{
  return c=='&' || c=='|' || c=='!';
}

/**
 * checks if the given char c is a letter or underscore
 */
static bool isAlpha(const char c)
{
  return (c>='A' && c<='Z') || (c>='a' && c<='z') || c=='_';
}

static bool isAlphaNumSpec(const char c)
{
  return isAlpha(c) || (c>='0' && c<='9') || c=='-' || c=='.' || (static_cast<unsigned char>(c)>=0x80);
}

/**
 * returns the id of the given operator
 * returns -1 if the operator is not recognized
 */
int CondParser::getOperatorId(const DString &opName)
{
  // level 2
  if (opName=="&&") { return AND; }
  if (opName=="||") { return OR;  }

  // not operator
  if (opName=="!")  { return NOT; }

  return UNKNOWN_OP;
}

/**
 * Get next token in the current string expr.
 * Uses the data in m_expr pointed to by m_e to
 * produce m_tokenType and m_token, set m_err in case of an error
 */
void CondParser::getToken()
{
  m_tokenType = NOTHING;
  m_token.clear();

  //printf("\tgetToken e:{%c}, ascii=%i, col=%i\n", *e, *e, e-expr);

  // skip over whitespaces
  while (*m_e == ' ' || *m_e == '\t' || *m_e == '\n')     // space or tab or newline
  {
    m_e++;
  }

  // check for end of expression
  if (*m_e=='\0')
  {
    // token is still empty
    m_tokenType = DELIMITER;
    return;
  }

  // check for parentheses
  if (*m_e == '(' || *m_e == ')')
  {
    m_tokenType = DELIMITER;
    m_token += *m_e++;
    return;
  }

  // check for operators (delimiters)
  if (isDelimiter(*m_e))
  {
    m_tokenType = DELIMITER;
    while (isDelimiter(*m_e))
    {
      m_token += *m_e++;
    }
    return;
  }

  // check for variables
  if (isAlpha(*m_e))
  {
    m_tokenType = VARIABLE;
    while (isAlphaNumSpec(*m_e))
    {
      m_token += *m_e++;
    }
    return;
  }

  // something unknown is found, wrong characters -> a syntax error
  m_tokenType = UNKNOWN;
  while (*m_e)
  {
    m_token += *m_e++;
  }
  m_err = DString("Syntax error in part '")+m_token+"'";
  return;
}


/**
 * conditional operators AND and OR
 */
bool CondParser::parseLevel1()
{
  bool ans = parseLevel2();
  int opId = getOperatorId(m_token);

  while (opId==AND || opId==OR)
  {
    getToken();
    ans = evalOperator(opId, ans, parseLevel2());
    opId = getOperatorId(m_token);
  }

  return ans;
}

/**
 * NOT
 */
bool CondParser::parseLevel2()
{
  int opId = getOperatorId(m_token);
  if (opId == NOT)
  {
    getToken();
    return !parseLevel3();
  }
  else
  {
    return parseLevel3();
  }
}


/**
 * parenthesized expression or variable
 */
bool CondParser::parseLevel3()
{
  // check if it is a parenthesized expression
  if (m_tokenType == DELIMITER)
  {
    if (m_token=="(")
    {
      getToken();
      bool ans = parseLevel1();
      if (m_tokenType!=DELIMITER || m_token!=")")
      {
        m_err="Parenthesis ) missing";
        return false;
      }
      getToken();
      return ans;
    }
  }

  // if not parenthesized then the expression is a variable
  return parseVar();
}


bool CondParser::parseVar()
{
  bool ans = false;
  switch (m_tokenType)
  {
    case VARIABLE:
      // this is a variable
      ans = evalVariable(m_token);
      getToken();
      break;

    default:
      // syntax error or unexpected end of expression
      if (m_token.empty())
      {
        m_err="Unexpected end of expression";
        return false;
      }
      else
      {
        m_err="Value expected";
        return false;
      }
      break;
  }
  return ans;
}

/**
 * evaluate an operator for given values
 */
bool CondParser::evalOperator(int opId, bool lhs, bool rhs)
{
  switch (opId)
  {
    // level 2
    case AND: return lhs && rhs;
    case OR:  return lhs || rhs;
  }

  m_err = "Internal error unknown operator: id="+DString().setNum(opId);
  return false;
}

/**
 * evaluate a variable
 */
bool CondParser::evalVariable(const DString &varName)
{
  if (varName == "YES") return true;
  if (varName == "NO") return false;
  StringVector list = Config_getList(ENABLED_SECTIONS);
  return std::find(list.begin(),list.end(),varName.str())!=list.end();
}

static DString getConfig(const DString &fileName,int lineNr, const DString &expr)
{
  if (expr.empty())
  {
    return error_str;
  }
  ConfigOption * opt = ConfigImpl::instance()->get(expr);
  if (opt)
  {
    switch (opt->kind())
    {
      case ConfigOption::O_Bool:
        return((static_cast<ConfigBool*>(opt)->valueRef())? "YES" : "NO");
      case ConfigOption::O_String:
        // due to the fact that there can be any character in the string
        warn(fileName,lineNr,
             "String setting '{}' not possible in conditional statement, ignored", expr);
        return error_str;
      case ConfigOption::O_Enum:
        return(*(static_cast<ConfigEnum*>(opt)->valueRef()));
      case ConfigOption::O_Int:
        return (DString().setNum(*(static_cast<ConfigInt*>(opt)->valueRef())));
      case ConfigOption::O_List:
        warn(fileName,lineNr,
             "List setting '{}' not possible in conditional statement, ignored", expr);
        return error_str;
      case ConfigOption::O_Obsolete:
        warn(fileName,lineNr,
             "Obsolete setting '{}' not possible in conditional statement, ignored", expr);
        return error_str;
      case ConfigOption::O_Disabled:
        warn(fileName,lineNr,
             "Disabled setting '{}' not possible in conditional statement, ignored", expr);
        return error_str;
      case ConfigOption::O_Info:
        warn(fileName,lineNr,
             "Info setting '{}' not possible in conditional statement, ignored", expr);
        return error_str;
      default:
        warn(fileName,lineNr,
             "Unknown error occurrence '{}', ignored", expr);
        return error_str;
    }
  }
  else
  {
    warn(fileName,lineNr,
         "Unknown setting '{}' not possible in conditional statement, ignored", expr);
    return error_str;
  }
}

static DString resolveConfig(const DString &fileName,int lineNr, const DString &expr)
{
  if (expr.empty())
  {
    return "";
  }

  DString loc_expr;
  signed char c = 0;
  const char *p=expr.data();
  while ((c=*p++)!=0)
  {
    switch(c)
    {
      case '\\':
      case '@':
        if (*p == 'd' && DString(p).startsWith("doxyconfig"))
        {
          p+=10; // skip doxyconfig
          while (*p==' ' || *p=='\t') {p++;}
          DString bufConfig;
          while ((c=*p++)!=0)
          {
             if ((c>='A' && c<='Z') || (c>='0' && c<='9') || c=='_') bufConfig += c;
             else
             {
               break;
             }
          }
          p--;
          loc_expr += getConfig(fileName, lineNr, bufConfig);
        }
        break;
      default:
        loc_expr += c;
        break;
    }
  }
  return loc_expr;
}
