/*
 * 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.ArrayList;
import java.util.Collection;
import java.util.List;
import org.eclipse.persistence.jpa.jpql.WordParser;
import org.eclipse.persistence.jpa.jpql.utility.iterable.ListIterable;
import org.eclipse.persistence.jpa.jpql.utility.iterable.SnapshotCloneListIterable;

/**
 * An identification variable followed by the navigation operator (.) and a state field or
 * association field is a path expression. The type of the path expression is the type computed as
 * the result of navigation; that is, the type of the state field or association field to which the
 * expression navigates.
 *
 * @see CollectionValuedPathExpression
 * @see IdentificationVariable
 *
 * @version 2.5
 * @since 2.3
 * @author Pascal Filion
 */
public abstract class AbstractPathExpression extends AbstractExpression {

    /**
     * Determines whether the path ends with a dot or not.
     */
    private boolean endsWithDot;

    /**
     * The identification variable that starts the path expression, which can be a sample {@link
     * IdentificationVariable identification variable}, an {@link EntryExpression entry expression},
     * a {@link ValueExpression value expression} or a {@link KeyExpression key expression}.
     */
    private AbstractExpression identificationVariable;

    /**
     * The state field path in a ordered list of string segments.
     */
    private List<String> paths;

    /**
     * The cached number of segments representing the path expression.
     */
    private int pathSize;

    /**
     * Determines whether the path starts with a dot or not.
     */
    private boolean startsWithDot;

    /**
     * Creates a new <code>AbstractPathExpression</code>.
     *
     * @param parent The parent of this expression
     * @param identificationVariable The identification variable that was already parsed, which means
     * the beginning of the parsing should start with a dot
     */
    protected AbstractPathExpression(AbstractExpression parent, AbstractExpression identificationVariable) {
        super(parent);
        this.pathSize = -1;
        this.identificationVariable = identificationVariable;
        this.identificationVariable.setParent(this);
    }

    /**
     * Creates a new <code>AbstractPathExpression</code>.
     *
     * @param parent The parent of this expression
     * @param identificationVariable The identification variable that was already parsed, which means
     * the beginning of the parsing should start with a dot
     * @param paths The path expression that is following the identification variable
     */
    public AbstractPathExpression(AbstractExpression parent,
                                  AbstractExpression identificationVariable,
                                  String paths) {

        super(parent, paths);
        this.pathSize = -1;
        this.identificationVariable = identificationVariable;
        this.identificationVariable.setParent(this);
    }

    /**
     * Creates a new <code>AbstractPathExpression</code>.
     *
     * @param parent The parent of this expression
     * @param paths The path expression
     */
    protected AbstractPathExpression(AbstractExpression parent, String paths) {
        super(parent, paths);
        this.pathSize = -1;
    }

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

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

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

        checkPaths();

        if (!hasVirtualIdentificationVariable()) {
            children.add(identificationVariable);
        }

        children.add(buildStringExpression(getText()));
    }

    private void checkPaths() {

        // Nothing to do
        if (paths != null) {
            return;
        }

        paths = new ArrayList<String>();
        String text = getText();
        char character = '\0';
        StringBuilder singlePath = new StringBuilder();

        // Extract each path from the text
        for (int index = 0, count = text.length(); index < count; index++) {

            character = text.charAt(index);

            // Make sure the identification variable is handled
            // correctly if it was passed during instantiation
            if (index == 0) {

                // No identification variable was passed during instantiation
                if (identificationVariable == null) {

                    // The path starts with '.'
                    startsWithDot = (character == DOT);

                    // Start appending to the current single path
                    if (!startsWithDot) {
                        singlePath.append(character);
                    }
                }
                // The identification variable was passed during instantiation,
                // add its parsed text as a path, it's assume the character is a dot
                else if (!identificationVariable.isNull() &&
                         !identificationVariable.isVirtual()) {

                    paths.add(identificationVariable.toParsedText());
                }
                // Start appending to the current single path
                else {
                    singlePath.append(character);
                }
            }
            else {

                // Append the character and continue
                if (character != DOT) {
                    singlePath.append(character);
                }
                // Scanning a '.'
                else {

                    // Store the current single path
                    paths.add(singlePath.toString());

                    // Clean the buffer
                    singlePath.setLength(0);
                }
            }
        }

        // Check if the last character is a '.'
        endsWithDot = (character == DOT);

        // Make sure the last path is added to the list
        if (singlePath.length() > 0) {
            paths.add(singlePath.toString());
        }

        // Cache the size
        pathSize = paths.size();

        // The identification variable can never be null
        if (identificationVariable == null) {
            if (startsWithDot || !endsWithDot && (pathSize == 1)) {
                identificationVariable = buildNullExpression();
            }
            else {
                identificationVariable = new IdentificationVariable(this, paths.get(0));
            }
        }
    }

    /**
     * Determines whether the path ends with a dot or not.
     *
     * @return <code>true</code> if the path ends with a dot; <code>false</code> otherwise
     */
    public final boolean endsWithDot() {
        checkPaths();
        return endsWithDot;
    }

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

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

        return super.findQueryBNF(expression);
    }

    /**
     * Returns the identification variable that starts the path expression, which can be a sample
     * identification variable, a map value, map key or map entry expression.
     *
     * @return The root of the path expression
     */
    public final Expression getIdentificationVariable() {
        checkPaths();
        return identificationVariable;
    }

    /**
     * Returns the specified segment of the state field path.
     *
     * @param index The 0-based segment index
     * @return The specified segment
     */
    public final String getPath(int index) {
        checkPaths();
        return paths.get(index);
    }

    /**
     * Determines whether the identification variable was parsed.
     *
     * @return <code>true</code> the identification variable was parsed; <code>false</code> otherwise
     */
    public final boolean hasIdentificationVariable() {
        checkPaths();
        return !identificationVariable.isNull() &&
               !identificationVariable.isVirtual();
    }

    /**
     * Determines whether the path's identification variable is virtual or not, meaning it's not part
     * of the query but is required for proper navigability.
     *
     * @return <code>true</code> if this identification variable was virtually created to fully
     * qualify path expression; <code>false</code> if it was parsed
     */
    public final boolean hasVirtualIdentificationVariable() {
        checkPaths();
        return identificationVariable.isVirtual();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected final void parse(WordParser wordParser, boolean tolerant) {
        wordParser.moveForward(getText());
    }

    /**
     * Returns the segments in the state field path in order.
     *
     * @return An <code>Iterator</code> over the segments of the state field path
     */
    public final ListIterable<String> paths() {
        checkPaths();
        return new SnapshotCloneListIterable<String>(paths);
    }

    /**
     * Returns the number of segments in the state field path.
     *
     * @return The number of segments
     */
    public final int pathSize() {
        checkPaths();
        return pathSize;
    }

    /**
     * Sets a virtual identification variable because the abstract schema name was parsed without
     * one. This is valid in an <b>UPDATE</b> and <b>DELETE</b> queries.
     *
     * @param variableName The identification variable that was generated to identify the "root" object
     */
    protected final void setVirtualIdentificationVariable(String variableName) {

        identificationVariable = new IdentificationVariable(this, variableName, true);

        rebuildActualText();
        rebuildParsedText();
    }

    /**
     * Determines whether the path starts with a dot or not.
     *
     * @return <code>true</code> if the path starts with a dot; <code>false</code> otherwise
     */
    public final boolean startsWithDot() {
        return startsWithDot;
    }

    /**
     * Returns a string representation from the given range.
     *
     * @param startIndex The beginning of the range to create the string representation
     * @param stopIndex When to stop creating the string representation, which is exclusive
     * @return The string representation of this path expression contained in the given range
     * @since 2.4
     */
    public String toParsedText(int startIndex, int stopIndex) {

        checkPaths();
        StringBuilder writer = new StringBuilder();

        for (int index = startIndex; index < stopIndex; index++) {
            writer.append(paths.get(index));

            if (index < stopIndex - 1) {
                writer.append(DOT);
            }
        }

        return writer.toString();
    }

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

        checkPaths();

        if (startsWithDot) {
            writer.append(DOT);
        }

        for (int index = 0, count = pathSize(); index < count; index++) {

            if (index > 0) {
                writer.append(DOT);
            }

            // Make sure to use the identification variable for proper formatting
            if ((index == 0) && hasIdentificationVariable()) {
                identificationVariable.toParsedText(writer, actual);
            }
            // Append a single path
            else {
                writer.append(paths.get(index));
            }
        }

        if (endsWithDot) {
            writer.append(DOT);
        }
    }
}
