/*
 * Copyright (C) 2021 Apple Inc. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
 * THE POSSIBILITY OF SUCH DAMAGE.
 */

#include "config.h"
#include "CSSCalcExpressionNodeParser.h"

#include "CSSCalcCategoryMapping.h"
#include "CSSCalcInvertNode.h"
#include "CSSCalcNegateNode.h"
#include "CSSCalcOperationNode.h"
#include "CSSCalcPrimitiveValueNode.h"
#include "CSSCalcSymbolTable.h"
#include "CSSCalcValue.h"
#include "CSSParserToken.h"
#include "CSSParserTokenRange.h"
#include "CSSPropertyParserHelpers.h"
#include "Logging.h"
#include <wtf/text/TextStream.h>

namespace WebCore {

static constexpr int maxExpressionDepth = 100;

// <https://drafts.csswg.org/css-values-4/#calc-syntax>:
// <calc()>  = calc( <calc-sum> )
// <min()>   = min( <calc-sum># )
// <max()>   = max( <calc-sum># )
// <clamp()> = clamp( <calc-sum>#{3} )
// <sin()>   = sin( <calc-sum> )
// <cos()>   = cos( <calc-sum> )
// <tan()>   = tan( <calc-sum> )
// <asin()>  = asin( <calc-sum> )
// <acos()>  = acos( <calc-sum> )
// <atan()>  = atan( <calc-sum> )
// <atan2()> = atan2( <calc-sum>, <calc-sum> )
// <pow()>   = pow( <calc-sum>, <calc-sum> )
// <sqrt()>  = sqrt( <calc-sum> )
// <hypot()> = hypot( <calc-sum># )
// <calc-sum> = <calc-product> [ [ '+' | '-' ] <calc-product> ]*
// <calc-product> = <calc-value> [ [ '*' | '/' ] <calc-value> ]*
// <calc-value> = <number> | <dimension> | <percentage> | ( <calc-sum> )
RefPtr<CSSCalcExpressionNode> CSSCalcExpressionNodeParser::parseCalc(CSSParserTokenRange tokens, CSSValueID function, bool allowsNegativePercentage)
{
    std::function<void(CSSCalcExpressionNode&)> setAllowsNegativePercentageReferenceIfNeeded = [&](CSSCalcExpressionNode& expression) {
        if (is<CSSCalcOperationNode>(expression)) {
            auto& operationNode = downcast<CSSCalcOperationNode>(expression);
            if (operationNode.isMinOrMaxNode())
                operationNode.setAllowsNegativePercentageReference();

            for (auto& child : operationNode.children())
                setAllowsNegativePercentageReferenceIfNeeded(child);
        }
    };

    tokens.consumeWhitespace();

    RefPtr<CSSCalcExpressionNode> result;
    bool ok = parseCalcFunction(tokens, function, 0, result);
    if (!ok || !tokens.atEnd())
        return nullptr;

    if (!result)
        return nullptr;

    LOG_WITH_STREAM(Calc, stream << "CSSCalcExpressionNodeParser::parseCalc " << prettyPrintNode(*result));

    if (allowsNegativePercentage)
        setAllowsNegativePercentageReferenceIfNeeded(*result);

    result = CSSCalcOperationNode::simplify(result.releaseNonNull());

    LOG_WITH_STREAM(Calc, stream << "CSSCalcExpressionNodeParser::parseCalc - after simplification " << prettyPrintNode(*result));

    return result;
}

char CSSCalcExpressionNodeParser::operatorValue(const CSSParserToken& token)
{
    if (token.type() == DelimiterToken)
        return token.delimiter();
    return 0;
}

enum ParseState {
    OK,
    TooDeep,
    NoMoreTokens
};

static const CSSCalcSymbolTable getConstantTable()
{
    return {
        { CSSValuePi, CSSUnitType::CSS_NUMBER, piDouble }, { CSSValueE, CSSUnitType::CSS_NUMBER, std::exp(1.0) },
        { CSSValueNegativeInfinity, CSSUnitType::CSS_NUMBER, -1 * std::numeric_limits<double>::infinity() },
        { CSSValueInfinity, CSSUnitType::CSS_NUMBER, std::numeric_limits<double>::infinity() },
        { CSSValueNaN, CSSUnitType::CSS_NUMBER, std::numeric_limits<double>::quiet_NaN() },
    };
}

static ParseState checkDepthAndIndex(int depth, CSSParserTokenRange tokens)
{
    if (tokens.atEnd())
        return NoMoreTokens;
    if (depth > maxExpressionDepth) {
        LOG_WITH_STREAM(Calc, stream << "Depth " << depth << " exceeded maxExpressionDepth " << maxExpressionDepth);
        return TooDeep;
    }
    return OK;
}

bool CSSCalcExpressionNodeParser::parseCalcFunction(CSSParserTokenRange& tokens, CSSValueID functionID, int depth, RefPtr<CSSCalcExpressionNode>& result)
{
    if (checkDepthAndIndex(depth, tokens) != OK)
        return false;

    // "arguments" refers to things between commas.
    unsigned minArgumentCount = 1;
    std::optional<unsigned> maxArgumentCount;

    switch (functionID) {
    case CSSValueClamp:
        minArgumentCount = 3;
        maxArgumentCount = 3;
        break;
    case CSSValueLog:
        maxArgumentCount = 2;
        break;
    case CSSValueRound:
        minArgumentCount = 2;
        maxArgumentCount = 3;
        break;
    case CSSValueMod:
    case CSSValueRem:
        minArgumentCount = 2;
        maxArgumentCount = 2;
        break;
    case CSSValueExp:
    case CSSValueSin:
    case CSSValueCos:
    case CSSValueTan:
    case CSSValueAcos:
    case CSSValueAsin:
    case CSSValueAtan:
    case CSSValueSign:
    case CSSValueAbs:
    case CSSValueCalc:
        maxArgumentCount = 1;
        break;
    case CSSValueAtan2:
        maxArgumentCount = 2;
        break;
    case CSSValuePow:
        minArgumentCount = 2;
        maxArgumentCount = 2;
        break;
    case CSSValueSqrt:
        maxArgumentCount = 1;
        break;
    default:
        break;
    }

    Vector<Ref<CSSCalcExpressionNode>> nodes;

    bool requireComma = false;
    unsigned argumentCount = 0;
    while (!tokens.atEnd()) {
        tokens.consumeWhitespace();
        if (requireComma && !CSSPropertyParserHelpers::consumeCommaIncludingWhitespace(tokens))
            return false;

        RefPtr<CSSCalcExpressionNode> node;
        if (!parseCalcSum(tokens, functionID, depth, node))
            return false;

        ++argumentCount;
        if (maxArgumentCount && argumentCount > maxArgumentCount.value())
            return false;

        nodes.append(node.releaseNonNull());
        requireComma = true;
    }
    
    if (argumentCount < minArgumentCount)
        return false;

    switch (functionID) {
    case CSSValueMin:
        result = CSSCalcOperationNode::createMinOrMaxOrClamp(CalcOperator::Min, WTFMove(nodes), m_destinationCategory);
        break;
    case CSSValueMax:
        result = CSSCalcOperationNode::createMinOrMaxOrClamp(CalcOperator::Max, WTFMove(nodes), m_destinationCategory);
        break;
    case CSSValueClamp:
        result = CSSCalcOperationNode::createMinOrMaxOrClamp(CalcOperator::Clamp, WTFMove(nodes), m_destinationCategory);
        break;
    case CSSValueSin:
        result = CSSCalcOperationNode::createTrig(CalcOperator::Sin, WTFMove(nodes));
        break;
    case CSSValueCos:
        result = CSSCalcOperationNode::createTrig(CalcOperator::Cos, WTFMove(nodes));
        break;
    case CSSValueTan:
        result = CSSCalcOperationNode::createTrig(CalcOperator::Tan, WTFMove(nodes));
        break;
    case CSSValueRound:
        result = CSSCalcOperationNode::createRound(WTFMove(nodes));
        break;
    case CSSValueMod:
        result = CSSCalcOperationNode::createStep(CalcOperator::Mod, WTFMove(nodes));
        break;
    case CSSValueRem:
        result = CSSCalcOperationNode::createStep(CalcOperator::Rem, WTFMove(nodes));
        break;
    case CSSValueWebkitCalc:
    case CSSValueCalc:
        result = CSSCalcOperationNode::createSum(WTFMove(nodes));
        break;
    case CSSValueLog:
        result = CSSCalcOperationNode::createLog(WTFMove(nodes));
        break;
    case CSSValueExp:
        result = CSSCalcOperationNode::createExp(WTFMove(nodes));
        break;
    case CSSValueAcos:
        result = CSSCalcOperationNode::createInverseTrig(CalcOperator::Acos, WTFMove(nodes));
        break;
    case CSSValueAsin:
        result = CSSCalcOperationNode::createInverseTrig(CalcOperator::Asin, WTFMove(nodes));
        break;
    case CSSValueAtan:
        result = CSSCalcOperationNode::createInverseTrig(CalcOperator::Atan, WTFMove(nodes));
        break;
    case CSSValueAtan2:
        result = CSSCalcOperationNode::createAtan2(WTFMove(nodes));
        break;
    case CSSValueAbs:
        result = CSSCalcOperationNode::createSign(CalcOperator::Abs, WTFMove(nodes));
        break;
    case CSSValueSign:
        result = CSSCalcOperationNode::createSign(CalcOperator::Sign, WTFMove(nodes));
        break;
    case CSSValuePow:
        result = CSSCalcOperationNode::createPowOrSqrt(CalcOperator::Pow, WTFMove(nodes));
        break;
    case CSSValueSqrt:
        result = CSSCalcOperationNode::createPowOrSqrt(CalcOperator::Sqrt, WTFMove(nodes));
        break;
    case CSSValueHypot:
        result = CSSCalcOperationNode::createHypot(WTFMove(nodes));
        break;
    default:
        break;
    }

    return !!result;
}

static bool checkRoundKeyword(CSSValueID functionID, RefPtr<CSSCalcExpressionNode>& result, CSSValueID constantID)
{
    if (functionID != CSSValueRound)
        return false;
    switch (constantID) {
    case CSSValueNearest:
        result = CSSCalcOperationNode::createRoundConstant(CalcOperator::Nearest);
        return true;
    case CSSValueToZero:
        result = CSSCalcOperationNode::createRoundConstant(CalcOperator::ToZero);
        return true;
    case CSSValueUp:
        result = CSSCalcOperationNode::createRoundConstant(CalcOperator::Up);
        return true;
    case CSSValueDown:
        result = CSSCalcOperationNode::createRoundConstant(CalcOperator::Down);
        return true;
    default:
        return false;
    }
}

bool CSSCalcExpressionNodeParser::parseValue(CSSParserTokenRange& tokens, CSSValueID functionID, RefPtr<CSSCalcExpressionNode>& result)
{
    auto makeCSSCalcPrimitiveValueNode = [&] (CSSUnitType type, double value) -> bool {
        if (calcUnitCategory(type) == CalculationCategory::Other)
            return false;
        
        result = CSSCalcPrimitiveValueNode::create(CSSPrimitiveValue::create(value, type));
        return true;
    };

    auto token = tokens.consumeIncludingWhitespace();

    switch (token.type()) {
    case IdentToken: {
        if (checkRoundKeyword(functionID, result, token.id()))
            return true;
        auto value = m_symbolTable.get(token.id());
        value = value ? value : getConstantTable().get(token.id());
        if (!value)
            return false;
        return makeCSSCalcPrimitiveValueNode(value->type, value->value);
    }

    case NumberToken:
    case PercentageToken:
    case DimensionToken:
        return makeCSSCalcPrimitiveValueNode(token.unitType(), token.numericValue());

    default:
        return false;
    }

    ASSERT_NOT_REACHED();
    return false;
}

bool CSSCalcExpressionNodeParser::parseCalcValue(CSSParserTokenRange& tokens, CSSValueID functionID, int depth, RefPtr<CSSCalcExpressionNode>& result)
{
    if (checkDepthAndIndex(depth, tokens) != OK)
        return false;

    auto findFunctionId = [&](CSSValueID& functionId) {
        if (tokens.peek().type() == LeftParenthesisToken) {
            functionId = CSSValueCalc;
            return true;
        }

        functionId = tokens.peek().functionId();
        return CSSCalcValue::isCalcFunction(functionId);
    };

    CSSValueID functionId;
    if (findFunctionId(functionId)) {
        CSSParserTokenRange innerRange = tokens.consumeBlock();
        tokens.consumeWhitespace();
        innerRange.consumeWhitespace();
        return parseCalcFunction(innerRange, functionId, depth + 1, result);
    }

    return parseValue(tokens, functionID, result);
}

bool CSSCalcExpressionNodeParser::parseCalcProduct(CSSParserTokenRange& tokens, CSSValueID functionID, int depth, RefPtr<CSSCalcExpressionNode>& result)
{
    if (checkDepthAndIndex(depth, tokens) != OK)
        return false;

    RefPtr<CSSCalcExpressionNode> firstValue;
    if (!parseCalcValue(tokens, functionID, depth, firstValue))
        return false;

    Vector<Ref<CSSCalcExpressionNode>> nodes;

    while (!tokens.atEnd()) {
        char operatorCharacter = operatorValue(tokens.peek());
        if (operatorCharacter != static_cast<char>(CalcOperator::Multiply) && operatorCharacter != static_cast<char>(CalcOperator::Divide))
            break;
        tokens.consumeIncludingWhitespace();
        
        RefPtr<CSSCalcExpressionNode> nextValue;
        if (!parseCalcValue(tokens, functionID, depth, nextValue) || !nextValue)
            return false;

        if (operatorCharacter == static_cast<char>(CalcOperator::Divide))
            nextValue = CSSCalcInvertNode::create(nextValue.releaseNonNull());

        if (firstValue)
            nodes.append(firstValue.releaseNonNull());

        nodes.append(nextValue.releaseNonNull());
    }

    if (nodes.isEmpty()) {
        result = WTFMove(firstValue);
        return !!result;
    }

    result = CSSCalcOperationNode::createProduct(WTFMove(nodes));
    return !!result;
}

bool CSSCalcExpressionNodeParser::parseCalcSum(CSSParserTokenRange& tokens, CSSValueID functionID, int depth, RefPtr<CSSCalcExpressionNode>& result)
{
    if (checkDepthAndIndex(depth, tokens) != OK)
        return false;

    RefPtr<CSSCalcExpressionNode> firstValue;
    if (!parseCalcProduct(tokens, functionID, depth, firstValue))
        return false;

    Vector<Ref<CSSCalcExpressionNode>> nodes;

    while (!tokens.atEnd()) {
        char operatorCharacter = operatorValue(tokens.peek());
        if (operatorCharacter != static_cast<char>(CalcOperator::Add) && operatorCharacter != static_cast<char>(CalcOperator::Subtract))
            break;

        if ((&tokens.peek() - 1)->type() != WhitespaceToken)
            return false; // calc(1px+ 2px) is invalid

        tokens.consume();
        if (tokens.peek().type() != WhitespaceToken)
            return false; // calc(1px +2px) is invalid

        tokens.consumeIncludingWhitespace();

        RefPtr<CSSCalcExpressionNode> nextValue;
        if (!parseCalcProduct(tokens, functionID, depth, nextValue) || !nextValue)
            return false;

        if (operatorCharacter == static_cast<char>(CalcOperator::Subtract))
            nextValue = CSSCalcNegateNode::create(nextValue.releaseNonNull());

        if (firstValue)
            nodes.append(firstValue.releaseNonNull());

        nodes.append(nextValue.releaseNonNull());
    }

    if (nodes.isEmpty()) {
        result = WTFMove(firstValue);
        return !!result;
    }

    result = CSSCalcOperationNode::createSum(WTFMove(nodes));
    return !!result;
}

}
