/*
 * Copyright (c) 2019, WSO2 Inc. (http://www.wso2.org) All Rights Reserved.
 *
 * WSO2 Inc. licenses this file to you under the Apache License,
 * Version 2.0 (the "License"); you may not use this file except
 * in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied. See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

package org.apache.synapse.commons.json.jsonprocessor.validators;

import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive;
import org.apache.synapse.commons.json.jsonprocessor.constants.ValidatorConstants;
import org.apache.synapse.commons.json.jsonprocessor.exceptions.ParserException;
import org.apache.synapse.commons.json.jsonprocessor.exceptions.ValidatorException;
import org.apache.synapse.commons.json.jsonprocessor.utils.DataTypeConverter;
import org.apache.synapse.commons.json.jsonprocessor.utils.JsonProcessorUtils;

import java.math.BigDecimal;

/**
 * validate numeric instances according to the given schema.
 */
public class NumericValidator {

    private static final String INTEGER_STRING = "integer";
    private static final String MINIMUM_VALUE = "minimum";
    private static final String MAXIMUM_VALUE = "maximum";
    private static final String EXCLUSIVE_MINIMUM = "exclusiveMinimum";
    private static final String EXCLUSIVE_MAXIMUM = "exclusiveMaximum";
    private static final String MULTIPLE_OF = "multipleOf";

    // use without instantiating.
    private NumericValidator() {

    }

    /**
     * Take JSON schema, number as a string input and validate.
     *
     * @param inputObject JSON schema.
     * @param value       numeric value
     * @return JsonPrimitive contains a number
     * @throws ParserException    Exception occurred in data type conversions.
     * @throws ValidatorException Exception occurred in schema validations.
     */
    public static JsonPrimitive validateNumeric(JsonObject inputObject, String value) throws ParserException,
            ValidatorException {
        Double multipleOf;
        if (value == null) {
            throw new ValidatorException("Expected a number but found null");
        }
        //replacing enclosing quotes
        value = JsonProcessorUtils.replaceEnclosingQuotes(value);
        if (isNumeric(value)) {
            String type = "";
            if (inputObject.has(ValidatorConstants.TYPE_KEY)) {
                if (inputObject.get(ValidatorConstants.TYPE_KEY).isJsonArray()) {
                    for (JsonElement typeValue : inputObject.get(ValidatorConstants.TYPE_KEY).getAsJsonArray()) {
                        type += JsonProcessorUtils.replaceEnclosingQuotes(typeValue.getAsString());
                    }
                } else {
                    type = JsonProcessorUtils.replaceEnclosingQuotes(
                            inputObject.get(ValidatorConstants.TYPE_KEY).getAsString());
                }
            }
            // handling multiples of condition
            Double doubleValue = DataTypeConverter.convertToDouble(value);
            if (inputObject.has(MULTIPLE_OF)) {
                multipleOf = DataTypeConverter.convertToDouble(JsonProcessorUtils.replaceEnclosingQuotes(
                        inputObject.get(MULTIPLE_OF).getAsString()));
                if (doubleValue % multipleOf != 0) {
                    throw new ValidatorException("Number " + value + " is not a multiple of " +
                            "" + multipleOf + ". multipleOf constraint in " + inputObject.toString() + " is violated " +
                            "by the input " + value);
                }
            }
            // handling maximum and minimum
            if (inputObject.has(MINIMUM_VALUE)) {
                String minimumString = JsonProcessorUtils.replaceEnclosingQuotes(
                        inputObject.get(MINIMUM_VALUE).getAsString());
                if (!minimumString.isEmpty() && doubleValue < DataTypeConverter.convertToDouble(minimumString)) {
                    throw new ValidatorException("Number " + value + " is less than the " +
                            "minimum allowed value" + ". minimumValue constraint in " + inputObject.toString() +
                            " is violated by the input " + ": " + value);
                }
            }
            if (inputObject.has(MAXIMUM_VALUE)) {
                String maximumString = JsonProcessorUtils.replaceEnclosingQuotes(
                        inputObject.get(MAXIMUM_VALUE).getAsString());
                if (!maximumString.isEmpty() && doubleValue > DataTypeConverter.convertToDouble(maximumString)) {
                    throw new ValidatorException("Number " + value + " is greater than the " +
                            "maximum allowed value. maximumValue constraint in " + inputObject.toString() +
                            " is violated by the input " + ": " + value);
                }
            }
            // handling exclusive maximum and minimum
            if (inputObject.has(EXCLUSIVE_MINIMUM)) {
                String minimumString = JsonProcessorUtils.replaceEnclosingQuotes(
                        inputObject.get(EXCLUSIVE_MINIMUM).getAsString());
                if (!minimumString.isEmpty() && doubleValue <= DataTypeConverter.convertToDouble(minimumString)) {
                    throw new ValidatorException("Number " + value + " is less than the " +
                            "minimum allowed value. ExclusiveMinimum constraint in " + inputObject.toString() +
                            " is violated by the " + "input : " + value );
                }
            }
            if (inputObject.has(EXCLUSIVE_MAXIMUM)) {
                String maximumString = JsonProcessorUtils.replaceEnclosingQuotes(
                        inputObject.get(EXCLUSIVE_MAXIMUM).getAsString());
                if (!maximumString.isEmpty() && doubleValue >= DataTypeConverter.convertToDouble(maximumString)) {
                    throw new ValidatorException("Number " + value + " is greater than the " +
                            "maximum allowed value. ExclusiveMaximum constraint in " +
                            inputObject.toString() + " is violated by the " + "input : " + value);
                }
            }
            // Enum validations
            if (inputObject.has(ValidatorConstants.ENUM)) {
                JsonArray enumElements = inputObject.getAsJsonArray(ValidatorConstants.ENUM);
                if (enumElements.size() > 0 && !enumElements.contains(new JsonPrimitive(doubleValue))) {
                    throw new ValidatorException("Number \"" + value + "\" not contains any " +
                            "element from the enum. Input " + value + " not contains any value from the enum in " +
                            inputObject.toString());
                }
            }
            //Const validation
            if (inputObject.has(ValidatorConstants.CONST) && !doubleValue.equals(inputObject.getAsJsonPrimitive
                    (ValidatorConstants.CONST).getAsDouble())) {
                throw new ValidatorException("Number \"" + value + "\" is not equal to the " +
                        "const value input " + value + " not contains the const defined in " + inputObject.toString());
            }
            // convert to integer of give value is a float
            if (type.contains(INTEGER_STRING)) {
                return new JsonPrimitive(DataTypeConverter.convertToBigInteger(value));
            } else {
                // this condition address both type number and empty json schemas
                int scale = value.length() - (value.indexOf(".") + 1);
                BigDecimal bigDecimalValue = new BigDecimal(value).setScale(scale);
                return new JsonPrimitive(bigDecimalValue);
            }
        }
        throw new ParserException("\"" + value + "\"" + " is not a number. " +
                "A number expected in the schema " + inputObject.toString() + " but received " + value);
    }

    /**
     * Check whether a given number is numeric. (alternative :- commons-lang3 isCreatable())
     * @param str input string.
     * @return number or not.
     */
    private static boolean isNumeric(String str) {
        try {
            Double.parseDouble(str);
        } catch (NumberFormatException nfe) {
            return false;
        }
        return true;
    }
}
