/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#include <math.h>

#include "mozilla/FloatingPoint.h"
#include "nsGkAtoms.h"
#include "nsWhitespaceTokenizer.h"
#include "txExpr.h"
#include "txIXPathContext.h"
#include "txNodeSet.h"
#include "txStringUtils.h"
#include "txXMLUtils.h"
#include "txXPathTreeWalker.h"

using namespace mozilla;

struct txCoreFunctionDescriptor {
  const int8_t mMinParams;
  const int8_t mMaxParams;
  const Expr::ResultType mReturnType;
  const nsStaticAtom* const mName;
};

// This must be ordered in the same order as txCoreFunctionCall::eType.
// If you change one, change the other.
static const txCoreFunctionDescriptor descriptTable[] = {
    {1, 1, Expr::NUMBER_RESULT, nsGkAtoms::count},         // COUNT
    {1, 1, Expr::NODESET_RESULT, nsGkAtoms::id},           // ID
    {0, 0, Expr::NUMBER_RESULT, nsGkAtoms::last},          // LAST
    {0, 1, Expr::STRING_RESULT, nsGkAtoms::localName},     // LOCAL_NAME
    {0, 1, Expr::STRING_RESULT, nsGkAtoms::namespaceUri},  // NAMESPACE_URI
    {0, 1, Expr::STRING_RESULT, nsGkAtoms::name},          // NAME
    {0, 0, Expr::NUMBER_RESULT, nsGkAtoms::position},      // POSITION

    {2, -1, Expr::STRING_RESULT, nsGkAtoms::concat},         // CONCAT
    {2, 2, Expr::BOOLEAN_RESULT, nsGkAtoms::contains},       // CONTAINS
    {0, 1, Expr::STRING_RESULT, nsGkAtoms::normalizeSpace},  // NORMALIZE_SPACE
    {2, 2, Expr::BOOLEAN_RESULT, nsGkAtoms::startsWith},     // STARTS_WITH
    {0, 1, Expr::STRING_RESULT, nsGkAtoms::string},          // STRING
    {0, 1, Expr::NUMBER_RESULT, nsGkAtoms::stringLength},    // STRING_LENGTH
    {2, 3, Expr::STRING_RESULT, nsGkAtoms::substring},       // SUBSTRING
    {2, 2, Expr::STRING_RESULT, nsGkAtoms::substringAfter},  // SUBSTRING_AFTER
    {2, 2, Expr::STRING_RESULT,
     nsGkAtoms::substringBefore},                       // SUBSTRING_BEFORE
    {3, 3, Expr::STRING_RESULT, nsGkAtoms::translate},  // TRANSLATE

    {0, 1, Expr::NUMBER_RESULT, nsGkAtoms::number},   // NUMBER
    {1, 1, Expr::NUMBER_RESULT, nsGkAtoms::round},    // ROUND
    {1, 1, Expr::NUMBER_RESULT, nsGkAtoms::floor},    // FLOOR
    {1, 1, Expr::NUMBER_RESULT, nsGkAtoms::ceiling},  // CEILING
    {1, 1, Expr::NUMBER_RESULT, nsGkAtoms::sum},      // SUM

    {1, 1, Expr::BOOLEAN_RESULT, nsGkAtoms::boolean},  // BOOLEAN
    {0, 0, Expr::BOOLEAN_RESULT, nsGkAtoms::_false},   // _FALSE
    {1, 1, Expr::BOOLEAN_RESULT, nsGkAtoms::lang},     // LANG
    {1, 1, Expr::BOOLEAN_RESULT, nsGkAtoms::_not},     // _NOT
    {0, 0, Expr::BOOLEAN_RESULT, nsGkAtoms::_true}     // _TRUE
};

/*
 * Evaluates this Expr based on the given context node and processor state
 * @param context the context node for evaluation of this Expr
 * @param ps the ContextState containing the stack information needed
 * for evaluation
 * @return the result of the evaluation
 */
nsresult txCoreFunctionCall::evaluate(txIEvalContext* aContext,
                                      txAExprResult** aResult) {
  *aResult = nullptr;

  if (!requireParams(descriptTable[mType].mMinParams,
                     descriptTable[mType].mMaxParams, aContext)) {
    return NS_ERROR_XPATH_BAD_ARGUMENT_COUNT;
  }

  nsresult rv = NS_OK;
  switch (mType) {
    case COUNT: {
      RefPtr<txNodeSet> nodes;
      rv = evaluateToNodeSet(mParams[0], aContext, getter_AddRefs(nodes));
      NS_ENSURE_SUCCESS(rv, rv);

      return aContext->recycler()->getNumberResult(nodes->size(), aResult);
    }
    case ID: {
      RefPtr<txAExprResult> exprResult;
      rv = mParams[0]->evaluate(aContext, getter_AddRefs(exprResult));
      NS_ENSURE_SUCCESS(rv, rv);

      RefPtr<txNodeSet> resultSet;
      rv = aContext->recycler()->getNodeSet(getter_AddRefs(resultSet));
      NS_ENSURE_SUCCESS(rv, rv);

      txXPathTreeWalker walker(aContext->getContextNode());

      if (exprResult->getResultType() == txAExprResult::NODESET) {
        txNodeSet* nodes =
            static_cast<txNodeSet*>(static_cast<txAExprResult*>(exprResult));
        int32_t i;
        for (i = 0; i < nodes->size(); ++i) {
          nsAutoString idList;
          txXPathNodeUtils::appendNodeValue(nodes->get(i), idList);
          nsWhitespaceTokenizer tokenizer(idList);
          while (tokenizer.hasMoreTokens()) {
            if (walker.moveToElementById(tokenizer.nextToken())) {
              resultSet->add(walker.getCurrentPosition());
            }
          }
        }
      } else {
        nsAutoString idList;
        exprResult->stringValue(idList);
        nsWhitespaceTokenizer tokenizer(idList);
        while (tokenizer.hasMoreTokens()) {
          if (walker.moveToElementById(tokenizer.nextToken())) {
            resultSet->add(walker.getCurrentPosition());
          }
        }
      }

      *aResult = resultSet;
      NS_ADDREF(*aResult);

      return NS_OK;
    }
    case LAST: {
      return aContext->recycler()->getNumberResult(aContext->size(), aResult);
    }
    case LOCAL_NAME:
    case NAME:
    case NAMESPACE_URI: {
      // Check for optional arg
      RefPtr<txNodeSet> nodes;
      if (!mParams.IsEmpty()) {
        rv = evaluateToNodeSet(mParams[0], aContext, getter_AddRefs(nodes));
        NS_ENSURE_SUCCESS(rv, rv);

        if (nodes->isEmpty()) {
          aContext->recycler()->getEmptyStringResult(aResult);

          return NS_OK;
        }
      }

      const txXPathNode& node =
          nodes ? nodes->get(0) : aContext->getContextNode();
      switch (mType) {
        case LOCAL_NAME: {
          StringResult* strRes = nullptr;
          rv = aContext->recycler()->getStringResult(&strRes);
          NS_ENSURE_SUCCESS(rv, rv);

          *aResult = strRes;
          txXPathNodeUtils::getLocalName(node, strRes->mValue);

          return NS_OK;
        }
        case NAMESPACE_URI: {
          StringResult* strRes = nullptr;
          rv = aContext->recycler()->getStringResult(&strRes);
          NS_ENSURE_SUCCESS(rv, rv);

          *aResult = strRes;
          txXPathNodeUtils::getNamespaceURI(node, strRes->mValue);

          return NS_OK;
        }
        case NAME: {
          // XXX Namespace: namespaces have a name
          if (txXPathNodeUtils::isAttribute(node) ||
              txXPathNodeUtils::isElement(node) ||
              txXPathNodeUtils::isProcessingInstruction(node)) {
            StringResult* strRes = nullptr;
            rv = aContext->recycler()->getStringResult(&strRes);
            NS_ENSURE_SUCCESS(rv, rv);

            *aResult = strRes;
            txXPathNodeUtils::getNodeName(node, strRes->mValue);
          } else {
            aContext->recycler()->getEmptyStringResult(aResult);
          }

          return NS_OK;
        }
        default: {
          MOZ_CRASH("Unexpected mType?!");
        }
      }
      MOZ_CRASH("Inner mType switch should have returned!");
    }
    case POSITION: {
      return aContext->recycler()->getNumberResult(aContext->position(),
                                                   aResult);
    }

      // String functions

    case CONCAT: {
      RefPtr<StringResult> strRes;
      rv = aContext->recycler()->getStringResult(getter_AddRefs(strRes));
      NS_ENSURE_SUCCESS(rv, rv);

      uint32_t i, len = mParams.Length();
      for (i = 0; i < len; ++i) {
        rv = mParams[i]->evaluateToString(aContext, strRes->mValue);
        NS_ENSURE_SUCCESS(rv, rv);
      }

      NS_ADDREF(*aResult = strRes);

      return NS_OK;
    }
    case CONTAINS: {
      nsAutoString arg2;
      rv = mParams[1]->evaluateToString(aContext, arg2);
      NS_ENSURE_SUCCESS(rv, rv);

      if (arg2.IsEmpty()) {
        aContext->recycler()->getBoolResult(true, aResult);
      } else {
        nsAutoString arg1;
        rv = mParams[0]->evaluateToString(aContext, arg1);
        NS_ENSURE_SUCCESS(rv, rv);

        aContext->recycler()->getBoolResult(FindInReadable(arg2, arg1),
                                            aResult);
      }

      return NS_OK;
    }
    case NORMALIZE_SPACE: {
      nsAutoString resultStr;
      if (!mParams.IsEmpty()) {
        rv = mParams[0]->evaluateToString(aContext, resultStr);
        NS_ENSURE_SUCCESS(rv, rv);
      } else {
        txXPathNodeUtils::appendNodeValue(aContext->getContextNode(),
                                          resultStr);
      }

      RefPtr<StringResult> strRes;
      rv = aContext->recycler()->getStringResult(getter_AddRefs(strRes));
      NS_ENSURE_SUCCESS(rv, rv);

      bool addSpace = false;
      bool first = true;
      strRes->mValue.SetCapacity(resultStr.Length());
      char16_t c;
      uint32_t src;
      for (src = 0; src < resultStr.Length(); src++) {
        c = resultStr.CharAt(src);
        if (XMLUtils::isWhitespace(c)) {
          addSpace = true;
        } else {
          if (addSpace && !first) strRes->mValue.Append(char16_t(' '));

          strRes->mValue.Append(c);
          addSpace = false;
          first = false;
        }
      }
      *aResult = strRes;
      NS_ADDREF(*aResult);

      return NS_OK;
    }
    case STARTS_WITH: {
      nsAutoString arg2;
      rv = mParams[1]->evaluateToString(aContext, arg2);
      NS_ENSURE_SUCCESS(rv, rv);

      bool result = false;
      if (arg2.IsEmpty()) {
        result = true;
      } else {
        nsAutoString arg1;
        rv = mParams[0]->evaluateToString(aContext, arg1);
        NS_ENSURE_SUCCESS(rv, rv);

        result = StringBeginsWith(arg1, arg2);
      }

      aContext->recycler()->getBoolResult(result, aResult);

      return NS_OK;
    }
    case STRING: {
      RefPtr<StringResult> strRes;
      rv = aContext->recycler()->getStringResult(getter_AddRefs(strRes));
      NS_ENSURE_SUCCESS(rv, rv);

      if (!mParams.IsEmpty()) {
        rv = mParams[0]->evaluateToString(aContext, strRes->mValue);
        NS_ENSURE_SUCCESS(rv, rv);
      } else {
        txXPathNodeUtils::appendNodeValue(aContext->getContextNode(),
                                          strRes->mValue);
      }

      NS_ADDREF(*aResult = strRes);

      return NS_OK;
    }
    case STRING_LENGTH: {
      nsAutoString resultStr;
      if (!mParams.IsEmpty()) {
        rv = mParams[0]->evaluateToString(aContext, resultStr);
        NS_ENSURE_SUCCESS(rv, rv);
      } else {
        txXPathNodeUtils::appendNodeValue(aContext->getContextNode(),
                                          resultStr);
      }
      rv = aContext->recycler()->getNumberResult(resultStr.Length(), aResult);
      NS_ENSURE_SUCCESS(rv, rv);

      return NS_OK;
    }
    case SUBSTRING: {
      nsAutoString src;
      rv = mParams[0]->evaluateToString(aContext, src);
      NS_ENSURE_SUCCESS(rv, rv);

      double start;
      rv = evaluateToNumber(mParams[1], aContext, &start);
      NS_ENSURE_SUCCESS(rv, rv);

      // check for NaN or +/-Inf
      if (std::isnan(start) || std::isinf(start) ||
          start >= src.Length() + 0.5) {
        aContext->recycler()->getEmptyStringResult(aResult);

        return NS_OK;
      }

      start = floor(start + 0.5) - 1;

      double end;
      if (mParams.Length() == 3) {
        rv = evaluateToNumber(mParams[2], aContext, &end);
        NS_ENSURE_SUCCESS(rv, rv);

        end += start;
        if (std::isnan(end) || end < 0) {
          aContext->recycler()->getEmptyStringResult(aResult);

          return NS_OK;
        }

        if (end > src.Length())
          end = src.Length();
        else
          end = floor(end + 0.5);
      } else {
        end = src.Length();
      }

      if (start < 0) start = 0;

      if (start > end) {
        aContext->recycler()->getEmptyStringResult(aResult);

        return NS_OK;
      }

      return aContext->recycler()->getStringResult(
          Substring(src, (uint32_t)start, (uint32_t)(end - start)), aResult);
    }
    case SUBSTRING_AFTER: {
      nsAutoString arg1;
      rv = mParams[0]->evaluateToString(aContext, arg1);
      NS_ENSURE_SUCCESS(rv, rv);

      nsAutoString arg2;
      rv = mParams[1]->evaluateToString(aContext, arg2);
      NS_ENSURE_SUCCESS(rv, rv);

      if (arg2.IsEmpty()) {
        return aContext->recycler()->getStringResult(arg1, aResult);
      }

      int32_t idx = arg1.Find(arg2);
      if (idx == kNotFound) {
        aContext->recycler()->getEmptyStringResult(aResult);

        return NS_OK;
      }

      const nsAString& result = Substring(arg1, idx + arg2.Length());
      return aContext->recycler()->getStringResult(result, aResult);
    }
    case SUBSTRING_BEFORE: {
      nsAutoString arg2;
      rv = mParams[1]->evaluateToString(aContext, arg2);
      NS_ENSURE_SUCCESS(rv, rv);

      if (arg2.IsEmpty()) {
        aContext->recycler()->getEmptyStringResult(aResult);

        return NS_OK;
      }

      nsAutoString arg1;
      rv = mParams[0]->evaluateToString(aContext, arg1);
      NS_ENSURE_SUCCESS(rv, rv);

      int32_t idx = arg1.Find(arg2);
      if (idx == kNotFound) {
        aContext->recycler()->getEmptyStringResult(aResult);

        return NS_OK;
      }

      return aContext->recycler()->getStringResult(StringHead(arg1, idx),
                                                   aResult);
    }
    case TRANSLATE: {
      nsAutoString src;
      rv = mParams[0]->evaluateToString(aContext, src);
      NS_ENSURE_SUCCESS(rv, rv);

      if (src.IsEmpty()) {
        aContext->recycler()->getEmptyStringResult(aResult);

        return NS_OK;
      }

      RefPtr<StringResult> strRes;
      rv = aContext->recycler()->getStringResult(getter_AddRefs(strRes));
      NS_ENSURE_SUCCESS(rv, rv);

      strRes->mValue.SetCapacity(src.Length());

      nsAutoString oldChars, newChars;
      rv = mParams[1]->evaluateToString(aContext, oldChars);
      NS_ENSURE_SUCCESS(rv, rv);

      rv = mParams[2]->evaluateToString(aContext, newChars);
      NS_ENSURE_SUCCESS(rv, rv);

      uint32_t i;
      int32_t newCharsLength = (int32_t)newChars.Length();
      for (i = 0; i < src.Length(); i++) {
        int32_t idx = oldChars.FindChar(src.CharAt(i));
        if (idx != kNotFound) {
          if (idx < newCharsLength)
            strRes->mValue.Append(newChars.CharAt((uint32_t)idx));
        } else {
          strRes->mValue.Append(src.CharAt(i));
        }
      }

      NS_ADDREF(*aResult = strRes);

      return NS_OK;
    }

      // Number functions

    case NUMBER: {
      double res;
      if (!mParams.IsEmpty()) {
        rv = evaluateToNumber(mParams[0], aContext, &res);
        NS_ENSURE_SUCCESS(rv, rv);
      } else {
        nsAutoString resultStr;
        txXPathNodeUtils::appendNodeValue(aContext->getContextNode(),
                                          resultStr);
        res = txDouble::toDouble(resultStr);
      }
      return aContext->recycler()->getNumberResult(res, aResult);
    }
    case ROUND: {
      double dbl;
      rv = evaluateToNumber(mParams[0], aContext, &dbl);
      NS_ENSURE_SUCCESS(rv, rv);

      if (std::isfinite(dbl)) {
        if (mozilla::IsNegative(dbl) && dbl >= -0.5) {
          dbl *= 0;
        } else {
          dbl = floor(dbl + 0.5);
        }
      }

      return aContext->recycler()->getNumberResult(dbl, aResult);
    }
    case FLOOR: {
      double dbl;
      rv = evaluateToNumber(mParams[0], aContext, &dbl);
      NS_ENSURE_SUCCESS(rv, rv);

      if (std::isfinite(dbl) && !mozilla::IsNegativeZero(dbl)) dbl = floor(dbl);

      return aContext->recycler()->getNumberResult(dbl, aResult);
    }
    case CEILING: {
      double dbl;
      rv = evaluateToNumber(mParams[0], aContext, &dbl);
      NS_ENSURE_SUCCESS(rv, rv);

      if (std::isfinite(dbl)) {
        if (mozilla::IsNegative(dbl) && dbl > -1)
          dbl *= 0;
        else
          dbl = ceil(dbl);
      }

      return aContext->recycler()->getNumberResult(dbl, aResult);
    }
    case SUM: {
      RefPtr<txNodeSet> nodes;
      nsresult rv =
          evaluateToNodeSet(mParams[0], aContext, getter_AddRefs(nodes));
      NS_ENSURE_SUCCESS(rv, rv);

      double res = 0;
      int32_t i;
      for (i = 0; i < nodes->size(); ++i) {
        nsAutoString resultStr;
        txXPathNodeUtils::appendNodeValue(nodes->get(i), resultStr);
        res += txDouble::toDouble(resultStr);
      }
      return aContext->recycler()->getNumberResult(res, aResult);
    }

      // Boolean functions

    case BOOLEAN: {
      bool result;
      nsresult rv = mParams[0]->evaluateToBool(aContext, result);
      NS_ENSURE_SUCCESS(rv, rv);

      aContext->recycler()->getBoolResult(result, aResult);

      return NS_OK;
    }
    case _FALSE: {
      aContext->recycler()->getBoolResult(false, aResult);

      return NS_OK;
    }
    case LANG: {
      txXPathTreeWalker walker(aContext->getContextNode());

      nsAutoString lang;
      bool found;
      do {
        found = walker.getAttr(nsGkAtoms::lang, kNameSpaceID_XML, lang);
      } while (!found && walker.moveToParent());

      if (!found) {
        aContext->recycler()->getBoolResult(false, aResult);

        return NS_OK;
      }

      nsAutoString arg;
      rv = mParams[0]->evaluateToString(aContext, arg);
      NS_ENSURE_SUCCESS(rv, rv);

      bool result =
          StringBeginsWith(lang, arg, nsCaseInsensitiveStringComparator) &&
          (lang.Length() == arg.Length() || lang.CharAt(arg.Length()) == '-');

      aContext->recycler()->getBoolResult(result, aResult);

      return NS_OK;
    }
    case _NOT: {
      bool result;
      rv = mParams[0]->evaluateToBool(aContext, result);
      NS_ENSURE_SUCCESS(rv, rv);

      aContext->recycler()->getBoolResult(!result, aResult);

      return NS_OK;
    }
    case _TRUE: {
      aContext->recycler()->getBoolResult(true, aResult);

      return NS_OK;
    }
  }

  aContext->receiveError(u"Internal error"_ns, NS_ERROR_UNEXPECTED);
  return NS_ERROR_UNEXPECTED;
}

Expr::ResultType txCoreFunctionCall::getReturnType() {
  return descriptTable[mType].mReturnType;
}

bool txCoreFunctionCall::isSensitiveTo(ContextSensitivity aContext) {
  switch (mType) {
    case COUNT:
    case CONCAT:
    case CONTAINS:
    case STARTS_WITH:
    case SUBSTRING:
    case SUBSTRING_AFTER:
    case SUBSTRING_BEFORE:
    case TRANSLATE:
    case ROUND:
    case FLOOR:
    case CEILING:
    case SUM:
    case BOOLEAN:
    case _NOT:
    case _FALSE:
    case _TRUE: {
      return argsSensitiveTo(aContext);
    }
    case ID: {
      return (aContext & NODE_CONTEXT) || argsSensitiveTo(aContext);
    }
    case LAST: {
      return !!(aContext & SIZE_CONTEXT);
    }
    case LOCAL_NAME:
    case NAME:
    case NAMESPACE_URI:
    case NORMALIZE_SPACE:
    case STRING:
    case STRING_LENGTH:
    case NUMBER: {
      if (mParams.IsEmpty()) {
        return !!(aContext & NODE_CONTEXT);
      }
      return argsSensitiveTo(aContext);
    }
    case POSITION: {
      return !!(aContext & POSITION_CONTEXT);
    }
    case LANG: {
      return (aContext & NODE_CONTEXT) || argsSensitiveTo(aContext);
    }
  }

  MOZ_ASSERT_UNREACHABLE("how'd we get here?");
  return true;
}

// static
bool txCoreFunctionCall::getTypeFromAtom(nsAtom* aName, eType& aType) {
  uint32_t i;
  for (i = 0; i < std::size(descriptTable); ++i) {
    if (aName == descriptTable[i].mName) {
      aType = static_cast<eType>(i);

      return true;
    }
  }

  return false;
}

#ifdef TX_TO_STRING
void txCoreFunctionCall::appendName(nsAString& aDest) {
  aDest.Append(descriptTable[mType].mName->GetUTF16String());
}
#endif
