/*
 * Copyright (c) 2011, 2018 Oracle and/or its affiliates. All rights reserved.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v. 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0,
 * or the Eclipse Distribution License v. 1.0 which is available at
 * http://www.eclipse.org/org/documents/edl-v10.php.
 *
 * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
 */

// Contributors:
//     Oracle - initial API and implementation
//
package org.eclipse.persistence.jpa.jpql.tools.model;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.eclipse.persistence.jpa.jpql.Assert;
import org.eclipse.persistence.jpa.jpql.ExpressionTools;
import org.eclipse.persistence.jpa.jpql.tools.model.query.AbsExpressionStateObject;
import org.eclipse.persistence.jpa.jpql.tools.model.query.AdditionExpressionStateObject;
import org.eclipse.persistence.jpa.jpql.tools.model.query.ArithmeticFactorStateObject;
import org.eclipse.persistence.jpa.jpql.tools.model.query.AvgFunctionStateObject;
import org.eclipse.persistence.jpa.jpql.tools.model.query.CoalesceExpressionStateObject;
import org.eclipse.persistence.jpa.jpql.tools.model.query.CollectionValuedPathExpressionStateObject;
import org.eclipse.persistence.jpa.jpql.tools.model.query.ConcatExpressionStateObject;
import org.eclipse.persistence.jpa.jpql.tools.model.query.CountFunctionStateObject;
import org.eclipse.persistence.jpa.jpql.tools.model.query.DateTimeStateObject;
import org.eclipse.persistence.jpa.jpql.tools.model.query.DivisionExpressionStateObject;
import org.eclipse.persistence.jpa.jpql.tools.model.query.EntityTypeLiteralStateObject;
import org.eclipse.persistence.jpa.jpql.tools.model.query.EnumTypeStateObject;
import org.eclipse.persistence.jpa.jpql.tools.model.query.FunctionExpressionStateObject;
import org.eclipse.persistence.jpa.jpql.tools.model.query.IdentificationVariableStateObject;
import org.eclipse.persistence.jpa.jpql.tools.model.query.IndexExpressionStateObject;
import org.eclipse.persistence.jpa.jpql.tools.model.query.InputParameterStateObject;
import org.eclipse.persistence.jpa.jpql.tools.model.query.LengthExpressionStateObject;
import org.eclipse.persistence.jpa.jpql.tools.model.query.LocateExpressionStateObject;
import org.eclipse.persistence.jpa.jpql.tools.model.query.MaxFunctionStateObject;
import org.eclipse.persistence.jpa.jpql.tools.model.query.ModExpressionStateObject;
import org.eclipse.persistence.jpa.jpql.tools.model.query.MultiplicationExpressionStateObject;
import org.eclipse.persistence.jpa.jpql.tools.model.query.NullIfExpressionStateObject;
import org.eclipse.persistence.jpa.jpql.tools.model.query.NumericLiteralStateObject;
import org.eclipse.persistence.jpa.jpql.tools.model.query.SizeExpressionStateObject;
import org.eclipse.persistence.jpa.jpql.tools.model.query.SqrtExpressionStateObject;
import org.eclipse.persistence.jpa.jpql.tools.model.query.StateFieldPathExpressionStateObject;
import org.eclipse.persistence.jpa.jpql.tools.model.query.StateObject;
import org.eclipse.persistence.jpa.jpql.tools.model.query.StringLiteralStateObject;
import org.eclipse.persistence.jpa.jpql.tools.model.query.SubExpressionStateObject;
import org.eclipse.persistence.jpa.jpql.tools.model.query.SubtractionExpressionStateObject;
import org.eclipse.persistence.jpa.jpql.tools.model.query.SumFunctionStateObject;
import org.eclipse.persistence.jpa.jpql.tools.model.query.TypeExpressionStateObject;
import static org.eclipse.persistence.jpa.jpql.parser.Expression.*;

/**
 * This abstract definition of a builder provides the support for creating expressions defined by a
 * <code>scalar expression</code>.
 *
 * @version 2.4
 * @since 2.4
 * @author Pascal Filion
 */
@SuppressWarnings({"unchecked", "nls"})
public abstract class AbstractScalarExpressionStateObjectBuilder<T extends IScalarExpressionStateObjectBuilder<T>> extends AbstractStateObjectBuilder
    implements IScalarExpressionStateObjectBuilder<T> {

    /**
     * Caches the {@link ICaseExpressionStateObjectBuilder} while it's been used.
     */
    private ICaseExpressionStateObjectBuilder caseBuilder;

    /**
     * The parent of the expression to build, which is only required when a JPQL fragment needs to
     * be parsed.
     */
    private StateObject parent;

    /**
     * Creates a new <code>AbstractScalarExpressionStateObjectBuilder</code>.
     *
     * @param parent The parent of the expression to build, which is only required when a JPQL
     * fragment needs to be parsed
     */
    protected AbstractScalarExpressionStateObjectBuilder(StateObject parent) {
        super();
        this.parent = parent;
    }

    /**
     * {@inheritDoc}
     */
    public T abs(T builder) {

        checkBuilder(builder);

        StateObject stateObject = new AbsExpressionStateObject(parent, pop());
        add(stateObject);
        return (T) this;
    }

    /**
     * {@inheritDoc}
     */
    public T add(T builder) {

        checkBuilder(builder);

        StateObject rightStateObject = pop();
        StateObject leftStateObject  = pop();

        StateObject stateObject = new AdditionExpressionStateObject(
            parent,
            leftStateObject,
            rightStateObject
        );

        add(stateObject);
        return (T) this;
    }

    protected void arithmetic(boolean plusSign) {
        StateObject stateObject = new ArithmeticFactorStateObject(parent, plusSign, pop());
        add(stateObject);
    }

    protected void avg(boolean distinct, String path) {
        StateObject stateObject = new AvgFunctionStateObject(parent, distinct, literal(path));
        add(stateObject);
    }

    /**
     * {@inheritDoc}
     */
    public T avg(String path) {
        avg(false, path);
        return (T) this;
    }

    /**
     * {@inheritDoc}
     */
    public T avgDistinct(String path) {
        avg(true, path);
        return (T) this;
    }

    protected StateObject buildCollectionPath(String path) {
        return new CollectionValuedPathExpressionStateObject(parent, path);
    }

    protected StateObject buildIdentificationVariable(String literal) {
        return new IdentificationVariableStateObject(parent, literal);
    }

    protected StateObject buildInputParameter(String parameter) {
        return new InputParameterStateObject(parent, parameter);
    }

    protected StateObject buildNumeric(Number number) {
        return new NumericLiteralStateObject(parent, number);
    }

    protected StateObject buildNumeric(String number) {
        return new NumericLiteralStateObject(parent, number);
    }

    protected StateObject buildStateFieldPath(String path) {
        return new StateFieldPathExpressionStateObject(parent, path);
    }

    protected StateObject buildStringLiteral(String literal) {
        return new StringLiteralStateObject(parent, literal);
    }

    /**
     * {@inheritDoc}
     */
    public T case_(ICaseExpressionStateObjectBuilder builder) {
        Assert.isEqual(caseBuilder, builder, "The Case expression builder is not the same as the current one");
        add(builder.buildStateObject());
        builder = null;
        return (T) this;
    }

    /**
     * {@inheritDoc}
     */
    public T coalesce(T builder1, T builder2, T... builders) {

        checkBuilders(builder1, builder2);
        checkBuilders(builders);

        List<StateObject> stateObjects = new ArrayList<StateObject>();
        stateObjects.addAll(stateObjects(builders));
        stateObjects.add(0, pop());
        stateObjects.add(0, pop());

        StateObject stateObject = new CoalesceExpressionStateObject(parent, stateObjects);
        add(stateObject);
        return (T) this;
    }

    /**
     * {@inheritDoc}
     */
    public T concat(T builder1, T builder2, T... builders) {

        checkBuilders(builder1, builder2);
        checkBuilders(builders);

        List<StateObject> stateObjects = new ArrayList<StateObject>();
        stateObjects.addAll(stateObjects(builders));
        stateObjects.add(0, pop());
        stateObjects.add(0, pop());

        StateObject stateObject = new ConcatExpressionStateObject(parent, stateObjects);
        add(stateObject);
        return (T) this;
    }

    protected void count(boolean distinct, String path) {
        StateObject stateObject = new CountFunctionStateObject(parent, distinct, literal(path));
        add(stateObject);
    }

    /**
     * {@inheritDoc}
     */
    public T count(String path) {
        count(false, path);
        return (T) this;
    }

    /**
     * {@inheritDoc}
     */
    public T countDistinct(String path) {
        count(true, path);
        return (T) this;
    }

    /**
     * {@inheritDoc}
     */
    public T currentDate() {
        return date(CURRENT_DATE);
    }

    /**
     * {@inheritDoc}
     */
    public T currentTime() {
        return date(CURRENT_TIME);
    }

    /**
     * {@inheritDoc}
     */
    public T currentTimestamp() {
        return date(CURRENT_TIMESTAMP);
    }

    /**
     * {@inheritDoc}
     */
    public T date(String jdbcDate) {
        StateObject stateObject = new DateTimeStateObject(parent, jdbcDate);
        add(stateObject);
        return (T) this;
    }

    /**
     * {@inheritDoc}
     */
    public T divide(T builder) {

        checkBuilder(builder);

        StateObject rightStateObject = pop();
        StateObject leftStateObject  = pop();

        StateObject stateObject = new DivisionExpressionStateObject(
            parent,
            leftStateObject,
            rightStateObject
        );

        add(stateObject);
        return (T) this;
    }

    /**
     * {@inheritDoc}
     */
    public T entityType(String entityTypeName) {
        StateObject stateObject = new EntityTypeLiteralStateObject(parent,entityTypeName);
        add(stateObject);
        return (T) this;
    }

    /**
     * {@inheritDoc}
     */
    public T enumLiteral(Enum<? extends Enum<?>> enumConstant) {
        StateObject stateObject = new EnumTypeStateObject(parent, enumConstant);
        add(stateObject);
        return (T) this;
    }

    /**
     * {@inheritDoc}
     */
    public T function(String identifier, String functionName, String... arguments) {

        StateObject stateObject = new FunctionExpressionStateObject(
            getParent(),
            identifier,
            functionName,
            literals(arguments)
        );

        add(stateObject);
        return (T) this;
    }

    /**
     * {@inheritDoc}
     */
    public T function(String identifier, String functionName, T... arguments) {

        checkBuilders(arguments);

        StateObject stateObject = new FunctionExpressionStateObject(
            getParent(),
            identifier,
            functionName,
            stateObjects(arguments)
        );

        add(stateObject);
        return (T) this;
    }

    /**
     * {@inheritDoc}
     */
    public ICaseExpressionStateObjectBuilder getCaseBuilder() {
        if (caseBuilder == null) {
            caseBuilder = getParent().getQueryBuilder().buildCaseExpressionStateObjectBuilder(parent);
        }
        return caseBuilder;
    }

    /**
     * Returns the parent of the expression to build, which is only required when a JPQL fragment
     * needs to be parsed.
     *
     * @return The parent
     */
    protected StateObject getParent() {
        return parent;
    }

    /**
     * {@inheritDoc}
     */
    public T index(String variable) {
        StateObject stateObject = new IndexExpressionStateObject(parent, variable);
        add(stateObject);
        return (T) this;
    }

    /**
     * {@inheritDoc}
     */
    public T length(T builder) {

        checkBuilder(builder);

        StateObject stateObject = new LengthExpressionStateObject(parent, pop());
        add(stateObject);
        return (T) this;
    }

    protected StateObject literal(String literal) {

        if ((literal != null) && (literal.length() > 0)) {

            char character = literal.charAt(0);

            // String literal
            if (ExpressionTools.isQuote(character)) {
                return buildStringLiteral(literal);
            }

            // Input parameter
            if (ExpressionTools.isParameter(character)) {
                return buildInputParameter(literal);
            }

            // State-field path expression
            if (literal.indexOf('.') > 0) {
                return buildStateFieldPath(literal);
            }

            // Identification variable
            return buildIdentificationVariable(literal);
        }

        // String literal
        return buildStringLiteral(literal);
    }

    protected List<StateObject> literals(String... literals) {
        List<StateObject> stateObjects = new ArrayList<StateObject>();
        for (String literal : literals) {
            stateObjects.add(literal(literal));
        }
        return stateObjects;
    }

    /**
     * {@inheritDoc}
     */
    public T locate(T parameter1, T parameter2) {
        return locate(parameter1, parameter2, null);
    }

    /**
     * {@inheritDoc}
     */
    public T locate(T parameter1, T parameter2, T parameter3) {

        checkBuilders(parameter1, parameter2);

        if (parameter3 != null) {
            checkBuilder(parameter3);
        }

        StateObject thirdStateObject  = (parameter3 != null) ? pop() : null;
        StateObject secondStateObject = pop();
        StateObject firstStateObject  = pop();

        StateObject stateObject = new LocateExpressionStateObject(
            parent,
            firstStateObject,
            secondStateObject,
            thirdStateObject
        );

        add(stateObject);
        return (T) this;
    }

    protected void max(boolean distinct, String path) {
        StateObject stateObject = new MaxFunctionStateObject(parent, distinct, literal(path));
        add(stateObject);
    }

    /**
     * {@inheritDoc}
     */
    public T max(String path) {
        max(false, path);
        return (T) this;
    }

    /**
     * {@inheritDoc}
     */
    public T maxDistinct(String path) {
        max(true, path);
        return (T) this;
    }

    protected void min(boolean distinct, String path) {
        StateObject stateObject = new MaxFunctionStateObject(parent, distinct, literal(path));
        add(stateObject);
    }

    /**
     * {@inheritDoc}
     */
    public T min(String path) {
        min(false, path);
        return (T) this;
    }

    /**
     * {@inheritDoc}
     */
    public T minDistinct(String path) {
        min(true, path);
        return (T) this;
    }

    /**
     * {@inheritDoc}
     */
    public T minus(T builder) {
        checkBuilders(builder);
        arithmetic(false);
        return (T) this;
    }

    /**
     * {@inheritDoc}
     */
    public T mod(T parameter1, T parameter2) {

        checkBuilders(parameter1, parameter2);

        StateObject secondStateObject = pop();
        StateObject firstStateObject  = pop();

        StateObject stateObject = new ModExpressionStateObject(
            parent,
            firstStateObject,
            secondStateObject
        );

        add(stateObject);
        return (T) this;
    }

    /**
     * {@inheritDoc}
     */
    public T multiply(T builder) {

        checkBuilders(builder);

        StateObject rightStateObject = pop();
        StateObject leftStateObject  = pop();

        StateObject stateObject = new MultiplicationExpressionStateObject(
            parent,
            leftStateObject,
            rightStateObject
        );

        add(stateObject);
        return (T) this;
    }

    /**
     * {@inheritDoc}
     */
    public T nullIf(T builder1, T builder2) {

        checkBuilders(builder1, builder2);

        StateObject rightStateObject = pop();
        StateObject leftStateObject  = pop();

        StateObject stateObject = new NullIfExpressionStateObject(
            parent,
            leftStateObject,
            rightStateObject
        );

        add(stateObject);
        return (T) this;
    }

    /**
     * {@inheritDoc}
     */
    public T numeric(Number number) {
        StateObject stateObject = buildNumeric(number);
        add(stateObject);
        return (T) this;
    }

    /**
     * {@inheritDoc}
     */
    public T numeric(String number) {
        StateObject stateObject = buildNumeric(number);
        add(stateObject);
        return (T) this;
    }

    /**
     * {@inheritDoc}
     */
    public T parameter(String parameter) {
        StateObject stateObject = buildInputParameter(parameter);
        add(stateObject);
        return (T) this;
    }

    /**
     * {@inheritDoc}
     */
    public T path(String path) {
        StateObject stateObject = buildStateFieldPath(path);
        add(stateObject);
        return (T) this;
    }

    /**
     * {@inheritDoc}
     */
    public T plus(T builder) {
        checkBuilders(builder);
        arithmetic(true);
        return (T) this;
    }

    /**
     * {@inheritDoc}
     */
    public T size(String path) {
        StateObject stateObject = new SizeExpressionStateObject(parent, buildCollectionPath(path));
        add(stateObject);
        return (T) this;
    }

    /**
     * {@inheritDoc}
     */
    public T sqrt(T builder) {

        checkBuilders(builder);

        StateObject stateObject = new SqrtExpressionStateObject(parent, pop());
        add(stateObject);
        return (T) this;
    }

    /**
     * Returns a list of the {@link StateObject StateObjects} that were previously created.
     *
     * @param count The number of {@link StateObject StateObjects} to move to the list
     * @return The list of {@link StateObject StateObjects} that were added to the stack
     */
    protected List<StateObject> stateObjects(int count) {

        if (count == 0) {
            return Collections.emptyList();
        }

        List<StateObject> items = new ArrayList<StateObject>(count);

        while (count-- > 0) {
            items.add(0, pop());
        }

        return items;
    }

    /**
     * Returns a list of the {@link StateObject StateObjects} that were previously created.
     *
     * @param builders The list of {@link IScalarExpressionStateObjectBuilder builders} is used to
     * determine how many {@link StateObject StateObjects} needs to be pulled out of the stack
     * @return The list of {@link StateObject StateObjects} that were added to the stack
     */
    protected List<StateObject> stateObjects(T... builders) {
        return stateObjects(builders.length);
    }

    /**
     * {@inheritDoc}
     */
    public T string(String literal) {
        StateObject stateObject = buildStringLiteral(literal);
        add(stateObject);
        return (T) this;
    }

    /**
     * {@inheritDoc}
     */
    public T sub(T builder) {

        checkBuilders(builder);

        StateObject stateObject = new SubExpressionStateObject(parent, pop());
        add(stateObject);
        return (T) this;
    }

    /**
     * {@inheritDoc}
     */
    public T subtract(T builder) {

        checkBuilders(builder);

        StateObject rightStateObject = pop();
        StateObject leftStateObject  = pop();

        StateObject stateObject = new SubtractionExpressionStateObject(
            parent,
            leftStateObject,
            rightStateObject
        );

        add(stateObject);
        return (T) this;
    }

    protected void sum(boolean distinct, String path) {
        StateObject stateObject = new SumFunctionStateObject(parent, distinct, literal(path));
        add(stateObject);
    }

    /**
     * {@inheritDoc}
     */
    public T sum(String path) {
        sum(false, path);
        return (T) this;
    }

    /**
     * {@inheritDoc}
     */
    public T sumDistinct(String path) {
        sum(true, path);
        return (T) this;
    }

    /**
     * {@inheritDoc}
     */
    public T type(String path) {
        StateObject stateObject = new TypeExpressionStateObject(parent, path);
        add(stateObject);
        return (T) this;
    }
}
