/*
 * 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.query;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.ListIterator;
import java.util.StringTokenizer;
import org.eclipse.persistence.jpa.jpql.ExpressionTools;
import org.eclipse.persistence.jpa.jpql.parser.AbstractPathExpression;
import org.eclipse.persistence.jpa.jpql.parser.IdentificationVariableBNF;
import org.eclipse.persistence.jpa.jpql.tools.model.IListChangeEvent.EventType;
import org.eclipse.persistence.jpa.jpql.tools.model.IListChangeListener;
import org.eclipse.persistence.jpa.jpql.tools.model.ListChangeEvent;
import org.eclipse.persistence.jpa.jpql.tools.spi.IManagedType;
import org.eclipse.persistence.jpa.jpql.tools.spi.IManagedTypeProvider;
import org.eclipse.persistence.jpa.jpql.tools.spi.IMapping;
import org.eclipse.persistence.jpa.jpql.tools.spi.IType;
import org.eclipse.persistence.jpa.jpql.tools.spi.ITypeDeclaration;
import org.eclipse.persistence.jpa.jpql.utility.CollectionTools;
import org.eclipse.persistence.jpa.jpql.utility.iterable.ListIterable;
import org.eclipse.persistence.jpa.jpql.utility.iterable.SnapshotCloneListIterable;
import static org.eclipse.persistence.jpa.jpql.parser.AbstractExpression.*;

/**
 * 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 AbstractPathExpression
 *
 * @version 2.5
 * @since 2.4
 * @author Pascal Filion
 */
@SuppressWarnings("nls")
public abstract class AbstractPathExpressionStateObject extends AbstractStateObject
                                                        implements ListHolderStateObject<String> {

    /**
     * The {@link StateObject} that represents the identification variable portion of the path
     * expression, which can be one of the following when the JPA version is 2.0 or later:
     * <ul>
     * <li>{@link IdentificationVariableStateObject}</li>
     * <li>{@link KeyExpressionStateObject}</li>
     * <li>{@link ValueExpressionStateObject}</li>
     * </ul>
     */
    private StateObject identificationVariable;

    /**
     *
     */
    private IManagedType managedType;

    /**
     *
     */
    private ArrayList<IMapping> mappings;

    /**
     * The list of segments, including the general identification variable.
     */
    private List<String> paths;

    /**
     *
     */
    private boolean resolved;

    /**
     * The {@link IType} of the object being mapped to this identification variable.
     */
    private IType type;

    /**
     * The {@link ITypeDeclaration} of the object being mapped to this identification variable.
     */
    private ITypeDeclaration typeDeclaration;

    /**
     * Notifies the identification variable property has changed.
     */
    public static final String IDENTIFICATION_VARIABLE_PROPERTY = "identificationVariable";

    /**
     * Notifies the content of the paths list has changed.
     */
    public static final String PATHS_LIST = "paths";

    /**
     * Creates a new <code>AbstractPathExpressionStateObject</code>.
     *
     * @param parent The parent of this state object, which cannot be <code>null</code>
     * @exception NullPointerException The given parent cannot be <code>null</code>
     */
    protected AbstractPathExpressionStateObject(StateObject parent) {
        super(parent);
    }

    /**
     * Creates a new <code>AbstractPathExpressionStateObject</code>.
     *
     * @param parent The parent of this state object, which cannot be <code>null</code>
     * @param path The path expression
     * @exception NullPointerException The given parent cannot be <code>null</code>
     */
    protected AbstractPathExpressionStateObject(StateObject parent, String path) {
        super(parent);
        setPath(path);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected void addChildren(List<StateObject> children) {

        super.addChildren(children);

        StateObject stateObject = getIdentificationVariable();

        if (stateObject != null) {
            children.add(stateObject);
        }
    }

    /**
     * {@inheritDoc}
     */
    @SuppressWarnings("unchecked")
    public String addItem(String item) {
        getChangeSupport().addItem(this, paths, PATHS_LIST, item);
        return item;
    }

    /**
     * {@inheritDoc}
     */
    public void addItems(List<? extends String> items) {
        getChangeSupport().addItems(this, paths, PATHS_LIST, items);
    }

    /**
     * {@inheritDoc}
     */
    public void addListChangeListener(String listName, IListChangeListener<String> listener) {
        getChangeSupport().addListChangeListener(listName, listener);
    }

    /**
     * Appends the given sequence of characters to the path expression. If the sequence does not
     * begin with a dot, then the first segment will be appended to the last segment and then new
     * segments will be created.
     *
     * @param text The sequence of characters to append to the path expression
     */
    public void append(String text) {

        StringBuilder word = new StringBuilder();
        int pathCount = paths.size();
        boolean appendToLastSegment = true;
        int startIndex = pathCount;

        for (int index = 0, count = text.length(); index < count; index++) {
            char character = text.charAt(index);

            if (character == DOT) {
                if (word.length() > 0) {
                    // Append the content of the buffer to the end of the last segment
                    if (appendToLastSegment) {
                        String currentPath = paths.get(pathCount - 1);
                        paths.set(pathCount - 1, currentPath + word);
                        startIndex = pathCount - 1;
                    }
                    // Add a new segment
                    else {
                        paths.add(word.toString());
                        pathCount++;
                    }

                    // Clear the buffer
                    word.delete(0, word.length());
                }

                appendToLastSegment = false;
                continue;
            }
            // Add the character to the buffer
            else {
                word.append(character);
            }
        }

        if (word.length() > 0) {
            // Append the content of the buffer to the end of the last segment
            if (appendToLastSegment) {
                String currentPath = paths.get(pathCount - 1);
                paths.set(pathCount - 1, currentPath + word);
                startIndex = pathCount - 1;
            }
            // Add a new segment
            else {
                paths.add(word.toString());
            }
        }

        ListChangeEvent<String> event = new ListChangeEvent<String>(this, paths, EventType.CHANGED, PATHS_LIST, paths, startIndex, itemsSize());
        getChangeSupport().fireListChangeEvent(event);
    }

    /**
     * {@inheritDoc}
     */
    public boolean canMoveDown(String item) {
        return false;
    }

    /**
     * {@inheritDoc}
     */
    public boolean canMoveUp(String item) {
        return false;
    }

    /**
     * Clears the values related to the managed type and type.
     */
    protected void clearResolvedObjects() {

        mappings.clear();

        resolved        = false;
        type            = null;
        typeDeclaration = null;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public AbstractPathExpression getExpression() {
        return (AbstractPathExpression) super.getExpression();
    }

    /**
     * Returns the {@link StateObject} representing 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 StateObject getIdentificationVariable() {
        if ((identificationVariable == null) && hasItems()) {
            identificationVariable = buildStateObject(getItem(0), IdentificationVariableBNF.ID);
        }
        return identificationVariable;
    }

    /**
     * {@inheritDoc}
     */
    public String getItem(int index) {
        return paths.get(index);
    }

    /**
     * Returns
     *
     * @return
     */
    public IManagedType getManagedType() {
        if (managedType == null) {
            managedType = resolveManagedType();
        }
        return managedType;
    }

    /**
     * Returns
     *
     * @return
     */
    public IMapping getMapping() {
        resolveMappings();
        return mappings.get(itemsSize() - 1);
    }

    /**
     * Retrieves the {@link IMapping} for the path at the given position.
     *
     * @param index The index of the path for which its {@link IMapping} should be retrieved, which
     * should start at 1 to skip the identification variable
     */
    public IMapping getMapping(int index) {
        resolveMappings();
        return mappings.get(index);
    }

    /**
     * Returns the string representation of the path expression. If the identification variable is
     * virtual, then it is not part of the result.
     *
     * @return The path expression, which is never <code>null</code>
     */
    public String getPath() {
        return toString();
    }

    /**
     * Returns the {@link IType} of the field handled by this object.
     *
     * @return Either the {@link IType} that was resolved by this state object or the {@link IType}
     * for {@link IType#UNRESOLVABLE_TYPE} if it could not be resolved
     */
    public IType getType() {
        if (type == null) {
            type = resolveType();
        }
        return type;
    }

    /**
     * Returns the {@link ITypeDeclaration} of the field handled by this object.
     *
     * @return Either the {@link ITypeDeclaration} that was resolved by this object or the {@link
     * ITypeDeclaration} for {@link IType#UNRESOLVABLE_TYPE} if it could not be resolved
     */
    public ITypeDeclaration getTypeDeclaration() {
        if (typeDeclaration == null) {
            typeDeclaration = resolveTypeDeclaration();
        }
        return typeDeclaration;
    }

    /**
     * Determines whether the identification variable is present.
     *
     * @return <code>true</code> the identification variable is present; <code>false</code> otherwise
     */
    public boolean hasIdentificationVariable() {
        return getIdentificationVariable() != null;
    }

    /**
     * {@inheritDoc}
     */
    public boolean hasItems() {
        return !paths.isEmpty();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected void initialize() {
        super.initialize();
        paths    = new ArrayList<String>();
        mappings = new ArrayList<IMapping>();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isEquivalent(StateObject stateObject) {

        if (!super.isEquivalent(stateObject)) {
            return false;
        }

        AbstractPathExpressionStateObject path = (AbstractPathExpressionStateObject) stateObject;

        if (!areEquivalent(getIdentificationVariable(), path.getIdentificationVariable())) {
            return false;
        }

        int index = itemsSize();

        if (index != path.itemsSize()) {
            return false;
        }

        // Skip index 0, it is already tested and two identification
        // variables with different case are equivalent
        while (--index > 0) {
            String path1 = getItem(index);
            String path2 = path.getItem(index);

            if (ExpressionTools.valuesAreDifferent(path1, path2)) {
                return false;
            }
        }

        return true;
    }

    /**
     * {@inheritDoc}
     */
    public ListIterable<String> items() {
        return new SnapshotCloneListIterable<String>(paths);
    }

    /**
     * {@inheritDoc}
     */
    public int itemsSize() {
        return paths.size();
    }

    /**
     * {@inheritDoc}
     */
    public String moveDown(String item) {
        throw new RuntimeException(getClass().getName() + " does not support moveDown(String).");
    }

    /**
     * {@inheritDoc}
     */
    public String moveUp(String item) {
        throw new RuntimeException(getClass().getName() + " does not support moveUp(String).");
    }

    /**
     * Removes the single path at the given index.
     *
     * @param index The position of the single path to remove. If the index is 0, then the
     * identification variable is nullified
     */
    public void removeItem(int index) {
        if (index == 0) {
            setIdentificationVariableInternally(null);
        }
        removeItem(getItem(index));
    }

    /**
     * {@inheritDoc}
     */
    public void removeItem(String item) {
        getChangeSupport().removeItem(this, paths, PATHS_LIST, item);
    }

    /**
     * {@inheritDoc}
     */
    public void removeItems(Collection<String> items) {
        getChangeSupport().removeItems(this, this.paths, PATHS_LIST, items);
    }

    /**
     * {@inheritDoc}
     */
    public void removeListChangeListener(String listName, IListChangeListener<String> listener) {
        getChangeSupport().removeListChangeListener(listName, listener);
    }

    /**
     * Resolves
     *
     * @return
     */
    protected abstract IManagedType resolveManagedType();

    /**
     * Resolves the {@link IMapping} objects that constitutes the path expression.
     */
    protected void resolveMappings() {

        if (!resolved) {
            resolved = true;
            IManagedTypeProvider provider = getManagedTypeProvider();
            IManagedType managedType = null;

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

                // Identification variable
                if (index == 0) {
                    StateObject stateObject = getIdentificationVariable();

                    // The identification variable is not set, which means the traversal can happen
                    if (stateObject != null) {
                        managedType = getDeclaration().findManagedType(stateObject);
                    }

                    mappings.add(null);
                }
                // Resolve the path expression after the identification variable
                else if (managedType != null) {

                    String path = getItem(index);

                    // Cache the mapping
                    IMapping mapping = managedType.getMappingNamed(path);
                    mappings.add(mapping);

                    // Continue by retrieving the managed type
                    if (mapping != null) {
                        managedType = provider.getManagedType(mapping.getType());
                    }
                    else {
                        managedType = null;
                    }
                }
                else {
                    mappings.add(null);
                }
            }
        }
    }

    /**
     * Resolves the {@link IType} of the property handled by this object.
     *
     * @return Either the {@link IType} that was resolved by this object or the {@link IType} for
     * {@link IType#UNRESOLVABLE_TYPE} if it could not be resolved
     */
    protected abstract IType resolveType();

    /**
     * Resolves the {@link ITypeDeclaration} of the property handled by this object.
     *
     * @return Either the {@link ITypeDeclaration} that was resolved by this object or the {@link
     * ITypeDeclaration} for {@link IType#UNRESOLVABLE_TYPE} if it could not be resolved
     */
    protected ITypeDeclaration resolveTypeDeclaration() {
        resolveMappings();
        return getMapping().getTypeDeclaration();
    }

    /**
     * Sets the {@link StateObject} representing the identification variable that starts the path
     * expression, which can be a sample identification variable, a map value, map key or map entry
     * expression.
     *
     * @param identificationVariable The root of the path expression
     */
    public void setIdentificationVariable(StateObject identificationVariable) {
        setIdentificationVariableInternally(identificationVariable);
        getChangeSupport().replaceItem(this, paths, PATHS_LIST, 0, identificationVariable.toString());
    }

    /**
     * Sets the {@link StateObject} representing the identification variable that starts the path
     * expression, which can be a sample identification variable, a map value, map key or map entry
     * expression. This method does not replace the first path in the list of paths.
     *
     * @param identificationVariable The root of the path expression
     */
    protected void setIdentificationVariableInternally(StateObject identificationVariable) {
        clearResolvedObjects();
        StateObject oldIdentificationVariable = this.identificationVariable;
        this.identificationVariable = parent(identificationVariable);
        firePropertyChanged(IDENTIFICATION_VARIABLE_PROPERTY, oldIdentificationVariable, identificationVariable);
    }

    /**
     * Changes the path expression with the list of segments, the identification variable will also
     * be updated with the first segment.
     *
     * @param path The new path expression
     */
    public void setPath(CharSequence path) {

        List<String> paths = new ArrayList<String>();

        for (StringTokenizer tokenizer = new StringTokenizer(path.toString(), ".", true); tokenizer.hasMoreTokens(); ) {
            String token = tokenizer.nextToken();
            if (!token.equals(".")) {
                paths.add(token);
            }
            else if (!tokenizer.hasMoreTokens()) {
                paths.add(ExpressionTools.EMPTY_STRING);
            }
        }

        setPaths(paths.listIterator());
    }

    /**
     * Replaces the existing path segment to become the given one.
     *
     * @param index The position of the path segment to replace
     * @param path The replacement
     */
    public void setPath(int index, String path) {

        if (index == 0) {
            setIdentificationVariableInternally(null);
        }
        else {
            clearResolvedObjects();
        }

        getChangeSupport().replaceItem(this, paths, PATHS_LIST, index, path);
    }

    /**
     * Changes the path expression with the list of segments, the identification variable will also
     * be updated with the first segment.
     *
     * @param paths The new path expression
     */
    public void setPaths(List<String> paths) {
        setIdentificationVariableInternally(null);
        getChangeSupport().replaceItems(this, this.paths, PATHS_LIST, paths);
    }

    /**
     * Changes the path expression with the list of segments, the identification variable will also
     * be updated with the first segment.
     *
     * @param paths The new path expression
     */
    public void setPaths(ListIterator<String> paths) {
        setPaths(CollectionTools.list(paths));
    }

    /**
     * Changes the path expression with the list of segments, the identification variable will also
     * be updated with the first segment.
     *
     * @param paths The new path expression
     */
    public void setPaths(String... paths) {
        setPaths(Arrays.asList(paths));
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected void toTextInternal(Appendable writer) throws IOException {

        StateObject stateObject = getIdentificationVariable();

        if (stateObject != null) {
            String variable = stateObject.toString();

            if (variable.length() > 0) {
                writer.append(variable);

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

        for (int index = 1, count = paths.size(); index < count; index++) {
            writer.append(paths.get(index));
            if (index < count - 1) {
                writer.append(DOT);
            }
        }
    }
}
