/*
 * Copyright (c) 2006, 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.parser;

import java.util.Collection;
import java.util.List;
import org.eclipse.persistence.jpa.jpql.ExpressionTools;
import org.eclipse.persistence.jpa.jpql.WordParser;

/**
 * An orderby_item must be one of the following:
 * <ol>
 * <li> A {@link StateFieldPathExpression state_field_path_expression} that evaluates to an orderable
 * state field of an entity or embeddable class abstract schema type designated in the SELECT clause
 * by one of the following:
 *   <ul>
 *   <li>A general_identification_variable
 *   <li>A single_valued_object_path_expression
 *   </ul>
 * <li>A {@link StateFieldPathExpression state_field_path_expression} that evaluates to the same
 * state field of the same entity or embeddable abstract schema type as a {@link StateFieldPathExpression
 * state_field_path_expression} in the <b>SELECT</b> clause
 * <li>A {@link ResultVariable result_variable} that refers to an orderable item in the <b>SELECT</b>
 * clause for which the same {@link ResultVariable result_variable} has been specified. This may be
 * the result of an aggregate_expression, a <code>scalar_expression</code>, or a {@link
 * StateFieldPathExpression state_field_path_expression} in the <b>SELECT</b> clause.
 * </ol>
 * <p>
 * The keyword <b>ASC</b> specifies that ascending ordering be used for the associated orderby_item;
 * the keyword <b>DESC</b> specifies that descending ordering be used. Ascending ordering is the
 * default.
 * <p>
 * The keyword <b>NULLS FIRST</b> specifies that nulls first ordering be used for the associated orderby_item;
 * the keyword <b>NULLS LAST</b> specifies that nulls last ordering be used. Ascending ordering is the
 * default.
 * <p>
 * JPA 1.0:
 * <div><b>BNF:</b> <code>orderby_item ::= state_field_path_expression [ ASC | DESC ]</code></div>
 * <p>
 * JPA 2.0
 * <div><b>BNF:</b> <code>orderby_item ::= state_field_path_expression | result_variable [ ASC | DESC ]</code></div>
 * <p>
 * EclipseLink 2.4:
 * <div><b>BNF:</b> <code>orderby_item ::= state_field_path_expression | result_variable [ ASC | DESC ] [ NULLS FIRST | NULLS LAST ]</code></div>
 * <p>
 *
 * @version 2.5
 * @since 2.3
 * @author Pascal Filion
 */
@SuppressWarnings("nls")
public final class OrderByItem extends AbstractExpression {

    /**
     * The {@link Expression} representing the order by expression.
     */
    private AbstractExpression expression;

    /**
     * The actual 'FIRST' identifier found in the string representation of the JPQL query.
     */
    private String firstIdentifier;

    /**
     * Determines whether a whitespace was parsed after the order by expression.
     */
    private boolean hasSpaceAfterExpression;

    /**
     * Determines whether a whitespace was parsed after <code><b>NULLS</b></code>.
     */
    private boolean hasSpaceAfterNulls;

    /**
     * Determines whether a whitespace was parsed after the ordering.
     */
    private boolean hasSpaceAfterOrdering;

    /**
     * The actual 'LAST' identifier found in the string representation of the JPQL query.
     */
    private String lastIdentifier;

    /**
     * The keyword <b>NULLS FIRST</b> specifies ordering null first; the keyword <b>NULLS LAST</b>
     * specifies ordering nulls last.
     */
    private NullOrdering nullOrdering;

    /**
     * The actual 'NULLS' identifier found in the string representation of the JPQL query.
     */
    private String nullsIdentifier;

    /**
     * The keyword <b>ASC</b> specifies that ascending ordering be used; the keyword <b>DESC</b>
     * specifies that descending ordering be used. Ascending ordering is the default.
     */
    private Ordering ordering;

    /**
     * The actual ordering identifier found in the string representation of the JPQL query.
     */
    private String orderingIdentifier;

    /**
     * Creates a new <code>OrderByItem</code>.
     *
     * @param parent The parent of this expression
     */
    public OrderByItem(AbstractExpression parent) {
        super(parent);
    }

    /**
     * {@inheritDoc}
     */
    public void accept(ExpressionVisitor visitor) {
        visitor.visit(this);
    }

    /**
     * {@inheritDoc}
     */
    public void acceptChildren(ExpressionVisitor visitor) {
        getExpression().accept(visitor);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected void addChildrenTo(Collection<Expression> children) {
        children.add(getExpression());
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected void addOrderedChildrenTo(List<Expression> children) {

        // Order By expression
        if (expression != null) {
            children.add(expression);
        }

        if (hasSpaceAfterExpression) {
            children.add(buildStringExpression(SPACE));
        }

        // Ordering type
        if (ordering != Ordering.DEFAULT) {
            children.add(buildStringExpression(ordering.toString()));
        }

        if (hasSpaceAfterOrdering) {
            children.add(buildStringExpression(SPACE));
        }

        // 'NULLS'
        if (nullsIdentifier != null) {
            children.add(buildStringExpression(NULLS));
        }

        if (hasSpaceAfterNulls) {
            children.add(buildStringExpression(SPACE));
        }

        // 'FIRST'
        if (firstIdentifier != null) {
            children.add(buildStringExpression(FIRST));
        }
        // 'LAST'
        else if (lastIdentifier != null) {
            children.add(buildStringExpression(LAST));
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public JPQLQueryBNF findQueryBNF(Expression expression) {

        if ((this.expression != null) && this.expression.isAncestor(expression)) {
            return getQueryBNF(InternalOrderByItemBNF.ID);
        }

        return super.findQueryBNF(expression);
    }

    /**
     * Returns the actual <code>null</code> ordering identifier found in the string representation of
     * the JPQL query, which has the actual case that was used.
     *
     * @return The <code>null</code> ordering identifier that was actually parsed, if one was present,
     * otherwise an empty string is returned
     */
    public String getActualNullOrdering() {

        // NULLS FIRST
        if ((nullsIdentifier != null) && (firstIdentifier != null)) {
            return nullsIdentifier + SPACE + firstIdentifier;
        }

        // NULLS LAST
        if ((nullsIdentifier != null) && (lastIdentifier != null)) {
            return nullsIdentifier + SPACE + lastIdentifier;
        }

        if (nullsIdentifier != null) {
            return nullsIdentifier;
        }

        if (firstIdentifier != null) {
            return firstIdentifier;
        }
        else if (lastIdentifier != null) {
            return lastIdentifier;
        }

        return ExpressionTools.EMPTY_STRING;
    }

    /**
     * Returns the actual ordering identifier found in the string representation of the JPQL query,
     * which has the actual case that was used.
     *
     * @return The ordering identifier that was actually parsed, if one was present, otherwise an
     * empty string is returned
     */
    public String getActualOrdering() {
        return (orderingIdentifier != null) ? orderingIdentifier : ExpressionTools.EMPTY_STRING;
    }

    /**
     * Returns the {@link Expression} that represents the order by expression.
     *
     * @return The expression that was parsed representing the order by expression
     */
    public Expression getExpression() {
        if (expression == null) {
            expression = buildNullExpression();
        }
        return expression;
    }

    /**
     * Returns the enum constant representing the null ordering type.
     *
     * @return The constant representing the null ordering, in the case the ordering was not parsed,
     * then {@link NullOrdering#DEFAULT} is returned
     */
    public NullOrdering getNullOrdering() {
        return nullOrdering;
    }

    /**
     * Returns the enum constant representing the ordering type.
     *
     * @return The constant representing the ordering, in the case the ordering was not parsed, then
     * {@link Ordering#DEFAULT} is returned
     */
    public Ordering getOrdering() {
        return ordering;
    }

    /**
     * {@inheritDoc}
     */
    public JPQLQueryBNF getQueryBNF() {
        return getQueryBNF(OrderByItemBNF.ID);
    }

    /**
     * Determines whether the order by expression was parsed.
     *
     * @return <code>true</code> if the order by expression was parsed; <code>false</code> otherwise
     */
    public boolean hasExpression() {
        return expression != null &&
              !expression.isNull();
    }

    /**
     * Determines whether the <code><b>NULLS</b></code> identifier was parsed.
     *
     * @return <code>true</code> if the <code><b>NULLS</b></code> identifier was parsed;
     *  <code>false</code> otherwise
     */
    public boolean hasNulls() {
        return nullsIdentifier != null;
    }

    /**
     * Determines whether <code><b>ASC</b></code> or <code><b>DESC</b></code> was parsed.
     *
     * @return <code>true</code> if the ordering status was parsed; <code>false</code> otherwise
     * @since 2.5
     */
    public boolean hasOrdering() {
        return ordering != Ordering.DEFAULT;
    }

    /**
     * Determines whether a whitespace was parsed after the order by expression.
     *
     * @return <code>true</code> if there was a whitespace after the order by expression;
     * <code>false</code> otherwise
     */
    public boolean hasSpaceAfterExpression() {
        return hasSpaceAfterExpression;
    }

    /**
     * Determines whether a whitespace was parsed after the <code>NULLS</code> identifier. If the
     * composite identifier was fully parsed - <code>NULLS FIRST</code> or <code>NULLS LAST</code> -
     * then this is always <code>true</code>, but if only <code>NULLS</code> was parsed, then this
     * can be useful to determine if there was a space after.
     *
     * @return <code>true</code> if there was a whitespace after the <code>NULLS</code> identifier;
     * <code>false</code> otherwise
     */
    public boolean hasSpaceAfterNulls() {
        return hasSpaceAfterNulls;
    }

    /**
     * Determines whether a whitespace was parsed after the ordering.
     *
     * @return <code>true</code> if there was a whitespace after the ordering;
     * <code>false</code> otherwise
     */
    public boolean hasSpaceAfterOrdering() {
        return hasSpaceAfterOrdering;
    }

    /**
     * Determines whether the ordering was specified as being ascendant.
     *
     * @return <code>true</code> if <b>ASC</b> was parsed; <code>false</code> otherwise
     */
    public boolean isAscending() {
        return ordering == Ordering.ASC;
    }

    /**
     * Determines whether the ordering was not specified.
     *
     * @return <code>true</code> if no ordering was parsed; <code>false</code> otherwise
     */
    public boolean isDefault() {
        return ordering == Ordering.DEFAULT;
    }

    /**
     * Determines whether the ordering was specified as being descendant.
     *
     * @return <code>true</code> if <b>DESC</b> was parsed; <code>false</code> otherwise
     */
    public boolean isDescending() {
        return ordering == Ordering.DESC;
    }

    /**
     * Determines whether the ordering was specified as being nulls first.
     *
     * @return <code>true</code> if <b>NULLS FIRST</b> was parsed; <code>false</code> otherwise
     */
    public boolean isNullsFirst() {
        return nullOrdering == NullOrdering.NULLS_FIRST;
    }

    /**
     * Determines whether the ordering was specified as being nulls first.
     *
     * @return <code>true</code> if <b>NULLS LAST</b> was parsed; <code>false</code> otherwise
     */
    public boolean isNullsLast() {
        return nullOrdering == NullOrdering.NULLS_LAST;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected boolean isParsingComplete(WordParser wordParser, String word, Expression expression) {
        return word.equalsIgnoreCase(ASC)     ||
               word.equalsIgnoreCase(DESC)    ||
               word.equalsIgnoreCase("NULLS") ||
               super.isParsingComplete(wordParser, word, expression);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected void parse(WordParser wordParser, boolean tolerant) {

        // Parse the state field path expression
        expression = parse(wordParser, InternalOrderByItemBNF.ID, tolerant);

        hasSpaceAfterExpression = wordParser.skipLeadingWhitespace() > 0;

        // Parse ASC/DESC
        if (!wordParser.isTail()) {
            String word = wordParser.word();

            // Parse 'ASC'
            if (word.equalsIgnoreCase(ASC)) {
                ordering = Ordering.ASC;
                orderingIdentifier = wordParser.moveForward(ASC.length());
            }
            // Parse 'DESC'
            else if (word.equalsIgnoreCase(DESC)) {
                ordering = Ordering.DESC;
                orderingIdentifier = wordParser.moveForward(DESC.length());
            }
            else {
                ordering = Ordering.DEFAULT;
            }
        }
        else {
            ordering = Ordering.DEFAULT;
        }

        // Parse NULLS FIRST/NULLS LAST
        if (!wordParser.isTail()) {
            int count = wordParser.skipLeadingWhitespace();
            hasSpaceAfterOrdering = (count > 0);

            // Parse 'NULLS'
            if (wordParser.startsWithIdentifier(NULLS)) {
                nullsIdentifier = wordParser.moveForward(NULLS);
                hasSpaceAfterNulls = wordParser.skipLeadingWhitespace() > 0;
            }

            // Parse 'FIRT'
            if (wordParser.startsWithIdentifier(FIRST)) {
                firstIdentifier = wordParser.moveForward(FIRST);
            }
            // Parse 'LAST'
            else if (wordParser.startsWithIdentifier(LAST)) {
                lastIdentifier = wordParser.moveForward(LAST);
            }

            if ((nullsIdentifier != null) && (firstIdentifier != null)) {
                nullOrdering = NullOrdering.NULLS_FIRST;
            }
            else if ((nullsIdentifier != null) && (lastIdentifier != null)) {
                nullOrdering = NullOrdering.NULLS_LAST;
            }
            else {
                nullOrdering = NullOrdering.DEFAULT;
            }
        }
        else {
            nullOrdering = NullOrdering.DEFAULT;
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected void toParsedText(StringBuilder writer, boolean actual) {

        // Order By expression
        if (expression != null) {
            expression.toParsedText(writer, actual);
        }

        if (hasSpaceAfterExpression) {
            writer.append(SPACE);
        }

        // Ordering type
        if (ordering != Ordering.DEFAULT) {
            writer.append(actual ? orderingIdentifier : ordering.name());
        }

        if (hasSpaceAfterOrdering) {
            writer.append(SPACE);
        }

        // 'NULLS'
        if (nullsIdentifier != null) {
            writer.append(actual? nullsIdentifier : NULLS);
        }

        if (hasSpaceAfterNulls) {
            writer.append(SPACE);
        }

        // 'FIRST'
        if (firstIdentifier != null) {
            writer.append(actual? firstIdentifier : FIRST);
        }
        // 'LAST'
        else if (lastIdentifier != null) {
            writer.append(actual? lastIdentifier : LAST);
        }
    }

    /**
     * This enumeration lists all the possible choices for ordering nulls in an item.
     */
    public enum NullOrdering {

        /**
         * The constant used when the ordering is not specify.
         */
        DEFAULT(ExpressionTools.EMPTY_STRING),

        /**
         * The constant for 'NULLS FIRST', which tells to order nulls first.
         */
        NULLS_FIRST(Expression.NULLS_FIRST),

        /**
         * The constant for 'NULLS LAST', which tells to order nulls last.
         */
        NULLS_LAST(Expression.NULLS_LAST);

        /**
         * The actual composite identifier.
         */
        private String identifier;

        /**
         * Creates a new <code>NullOrdering</code>.
         *
         * @param identifier The actual composite identifier
         */
        private NullOrdering(String identifier) {
            this.identifier = identifier;
        }

        /**
         * Returns the actual JPQL composite identifiers identified by this enum constant.
         *
         * @return The composite identifiers
         */
        public String getIdentifier() {
            return identifier;
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public String toString() {
            return identifier;
        }
    }

    /**
     * This enumeration lists all the possible choices for ordering an item.
     */
    public enum Ordering {

        /**
         * The constant for 'ASC', which tells to order the items in ascending ordering.
         */
        ASC,

        /**
         * The constant used when the ordering is not specify, the default is ascending ordering.
         */
        DEFAULT,

        /**
         * The constant for 'DESC', which tells to order the items in descending ordering.
         */
        DESC
    }
}
