/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF 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.openjpa.jdbc.kernel.exps;

import java.io.Serializable;
import java.sql.SQLException;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.ListIterator;
import java.util.Objects;

import org.apache.openjpa.jdbc.kernel.EagerFetchModes;
import org.apache.openjpa.jdbc.kernel.JDBCStoreQuery;
import org.apache.openjpa.jdbc.meta.ClassMapping;
import org.apache.openjpa.jdbc.meta.Discriminator;
import org.apache.openjpa.jdbc.meta.FieldMapping;
import org.apache.openjpa.jdbc.meta.Strategy;
import org.apache.openjpa.jdbc.meta.ValueMapping;
import org.apache.openjpa.jdbc.meta.strats.HandlerCollectionTableFieldStrategy;
import org.apache.openjpa.jdbc.meta.strats.HandlerHandlerMapTableFieldStrategy;
import org.apache.openjpa.jdbc.meta.strats.LRSMapFieldStrategy;
import org.apache.openjpa.jdbc.schema.Column;
import org.apache.openjpa.jdbc.schema.ForeignKey;
import org.apache.openjpa.jdbc.schema.Schemas;
import org.apache.openjpa.jdbc.sql.Joins;
import org.apache.openjpa.jdbc.sql.Result;
import org.apache.openjpa.jdbc.sql.SQLBuffer;
import org.apache.openjpa.jdbc.sql.Select;
import org.apache.openjpa.kernel.Broker;
import org.apache.openjpa.kernel.Filters;
import org.apache.openjpa.kernel.OpenJPAStateManager;
import org.apache.openjpa.kernel.StoreContext;
import org.apache.openjpa.kernel.exps.CandidatePath;
import org.apache.openjpa.kernel.exps.Context;
import org.apache.openjpa.lib.util.Localizer;
import org.apache.openjpa.meta.ClassMetaData;
import org.apache.openjpa.meta.FieldMetaData;
import org.apache.openjpa.meta.JavaTypes;
import org.apache.openjpa.meta.XMLMetaData;
import org.apache.openjpa.util.ImplHelper;
import org.apache.openjpa.util.UserException;

/**
 * A path represents a traversal into fields of a candidate object.
 *
 * @author Abe White
 */
public class PCPath extends CandidatePath implements JDBCPath {

    
    private static final long serialVersionUID = 1L;
    protected static final String TRUE = "1 = 1";
    protected static final String FALSE = "1 <> 1";

    private static final int PATH = 0;
    private static final int BOUND_VAR = 1;
    private static final int UNBOUND_VAR = 2;
    private static final int UNACCESSED_VAR = 3;
    private static final int XPATH = 4;
    private static final int OBJECT_PATH = 5;

    private static final Localizer _loc = Localizer.forPackage(PCPath.class);

    private final ClassMapping _candidate;
    private ClassMapping _class = null;
    private boolean _key = false;
    private int _type = PATH;
    private String _varName = null;
    private Class _cast = null;
    private boolean _cid = false;
    private FieldMetaData _xmlfield = null;
    private boolean _keyPath = false;
    private String _schemaAlias = null;

    /**
     * Return a path starting with the 'this' ptr.
     */
    public PCPath(ClassMapping type) {
        _candidate = type;
    }

    /**
     * Return a path starting from the given variable.
     */
    public PCPath(ClassMapping candidate, Variable var) {
        _candidate = candidate;
        _actions = new LinkedList();

        PCPath other = var.getPCPath();
        Action action = new Action();
        action.var = var.getName();
        if (other == null) {
            _type = UNBOUND_VAR;
            action.op = Action.UNBOUND_VAR;
            action.data = var;
            _schemaAlias = var.getName();
        } else {
            // bound variable; copy path
            _type = UNACCESSED_VAR;
            _actions.addAll(other._actions);
            _key = other._key;

            action.op = Action.VAR;
            action.data = var.getName();
            _schemaAlias = other._schemaAlias;
            _correlationVar = other._correlationVar;
        }
        _actions.add(action);
        _cast = var.getType(); // initial type is var type
    }

    /**
     * Return a path starting from the given subquery.
     */
    public PCPath(SubQ sub) {
        _candidate = sub.getCandidate();
        _actions = new LinkedList();

        Action action = new Action();
        action.op = Action.SUBQUERY;
        action.data = sub.getCandidateAlias();
        _actions.add(action);
        _cast = sub.getType(); // initial type is subquery type
        _varName = sub.getCandidateAlias();
    }

    @Override
    public void setSchemaAlias(String schemaAlias) {
        if (_schemaAlias == null)
            _schemaAlias = schemaAlias;
    }

    @Override
    public String getSchemaAlias() {
        return _schemaAlias;
    }

    @Override
    public void setSubqueryContext(Context context, String correlationVar) {
        Action action = lastFieldAction();
        if (action == null)
            return;
        action.context = context;
        _correlationVar = correlationVar;
    }

    /**
     * Set the path as a binding of the given variable.
     */
    public void addVariableAction(Variable var) {
        _varName = var.getName();
    }

    /**
     * Return true if this is a bound variable that has not been accessed
     * after binding. Useful for filters like
     * "coll.contains (var) &amp;&amp; var == null", which should really
     * just act like "coll.contains (null)".
     */
    public boolean isUnaccessedVariable() {
        return _type == UNACCESSED_VAR;
    }

    /**
     * Return whether this is a path involving a variable.
     */
    public boolean isVariablePath() {
        return _type != PATH;
    }

    /**
     * If this path is part of a contains clause, then alias it to the
     * proper contains id before initialization.
     */
    public synchronized void setContainsId(String id) {
        if (_cid)
            return;

        Action last = _actions == null ? null : (Action) _actions.getLast();
        if (last != null && last.op == Action.VAR && ((String)last.data).equals(last.var)) {
            _cid = true;
            return;
        }

        // treat it just like a unique variable
        Action action = new Action();
        action.op = Action.VAR;
        action.data = id;
        if (_actions == null)
            _actions = new LinkedList();
        _actions.add(action);
        _cid = true;
    }

    @Override
    public ClassMetaData getMetaData() {
        return _class;
    }

    @Override
    public void setMetaData(ClassMetaData meta) {
        _class = (ClassMapping) meta;
    }

    public boolean isKey() {
        return _key;
    }

    @Override
    public boolean isXPath() {
        return _type == XPATH;
    }

    public String getXPath() {
        StringBuilder xpath = new StringBuilder();
        Action action;
        Iterator itr = _actions.iterator();

        // Skip variable actions since they are not part of the xpath
        // until we reach the first xpath action.
        // The first xpath action maps to the root of an xml document.
        do
            action = (Action) itr.next();
        while (action.op != Action.GET_XPATH);

        // Skip XmlRootElement:
        // We can't rely on the accuracy of the name of the root element,
        // because it could be set to some default by JAXB XML Binding.
        // The caller(DBDictionary) should start with "/*" or "/*/",
        // we build the remaining xpath that follows the root element.
        while (itr.hasNext()) {
            action = (Action) itr.next();
            if (((XMLMetaData) action.data).getXmlname() != null)
                xpath.append(((XMLMetaData) action.data).getXmlname());
            else
                xpath.append("*");
            if (itr.hasNext())
                xpath.append("/");
        }
        return xpath.toString();
    }

    public String getPCPathString() {
        if (_actions == null)
            return (_varName == null) ? "" : _varName + ".";

        StringBuilder path = new StringBuilder();
        Action action;
        for (Object o : _actions) {
            action = (Action) o;
            if (action.op == Action.VAR || action.op == Action.SUBQUERY)
                path.append(action.data);
            else if (action.op == Action.UNBOUND_VAR)
                path.append(((Variable) action.data).getName());
            else
                path.append(((FieldMapping) action.data).getName());
            path.append('.');
        }
        if (_varName != null)
            path.append(_varName).append('.');
        return path.toString();
    }

    public ClassMapping getClassMapping(ExpState state) {
        PathExpState pstate = (PathExpState) state;
        if (pstate.field == null)
            return _class;
        if (_key) {
            if (pstate.field.getKey().getTypeCode() == JavaTypes.PC)
                return pstate.field.getKeyMapping().getTypeMapping();
            return null;
        }
        if (pstate.field.getElement().getTypeCode() == JavaTypes.PC) {
            if (pstate.field.isElementCollection() &&
                pstate.field.getElement().isEmbedded())
                pstate.isEmbedElementColl = true;
            return pstate.field.getElementMapping().getTypeMapping();
        }
        if (pstate.field.getTypeCode() == JavaTypes.PC)
            return pstate.field.getTypeMapping();
        return null;
    }

    public FieldMapping getFieldMapping(ExpState state) {
        return ((PathExpState) state).field;
    }

    public Column[] getColumns(ExpState state) {
        PathExpState pstate = (PathExpState) state;
        if (pstate.cols == null)
            pstate.cols = calculateColumns(pstate);
        return pstate.cols;
    }

    /**
     * The columns used by this path.
     */
    private Column[] calculateColumns(PathExpState pstate) {
        if (_key ||
            (_keyPath && pstate.field.getKey() != null &&
                !pstate.field.getKey().isEmbedded())) {
            if (!pstate.joinedRel
                && pstate.field.getKey().getValueMappedBy() != null)
                joinRelation(pstate, _key, false, false);
            else if (pstate.joinedRel
                && pstate.field.getKey().getTypeCode() == JavaTypes.PC)
                return pstate.field.getKeyMapping().getTypeMapping().
                    getPrimaryKeyColumns();
            return pstate.field.getKeyMapping().getColumns();
        }
        if (pstate.field != null) {
            switch (pstate.field.getTypeCode()) {
                case JavaTypes.MAP:
                case JavaTypes.ARRAY:
                case JavaTypes.COLLECTION:
                    ValueMapping elem = pstate.field.getElementMapping();
                    if (pstate.field.isElementCollection() && pstate.field.getElement().isEmbedded()) {
                        Strategy strategy = pstate.field.getStrategy();
                        if (strategy instanceof HandlerCollectionTableFieldStrategy) {
                            return ((HandlerCollectionTableFieldStrategy) strategy).
                                getElementColumns(elem.getTypeMapping());
                        } else if (strategy instanceof HandlerHandlerMapTableFieldStrategy) {
                            return ((HandlerHandlerMapTableFieldStrategy) strategy).
                            getValueColumns(elem.getTypeMapping());
                        }
                    }
                    if (pstate.joinedRel && elem.getTypeCode() == JavaTypes.PC)
                        return elem.getTypeMapping().getPrimaryKeyColumns();
                    if (elem.getColumns().length > 0)
                        return elem.getColumns();
                    return pstate.field.getColumns();
                case JavaTypes.PC:
                    if (pstate.joinedRel)
                        return pstate.field.getTypeMapping().
                            getPrimaryKeyColumns();
                    return pstate.field.getColumns();
                default:
                    return pstate.field.getColumns();
            }
        }
        return (_class == null) ? Schemas.EMPTY_COLUMNS
            : _class.getPrimaryKeyColumns();
    }

    @Override
    public boolean isVariable() {
        if (_actions == null)
            return false;
        Action action = (Action) _actions.getLast();
        return action.op == Action.UNBOUND_VAR || action.op == Action.VAR;
    }

    @Override
    public void get(FieldMetaData field, boolean nullTraversal) {
        if (_actions == null)
            _actions = new LinkedList();
        Action action = new Action();
        action.op = (nullTraversal) ? Action.GET_OUTER : Action.GET;
        action.data = field;
        _actions.add(action);
        if (_type == UNACCESSED_VAR)
            _type = BOUND_VAR;
        _cast = null;
        _key = false;
    }

    @Override
    public void get(FieldMetaData fmd, XMLMetaData meta) {
        if (_actions == null)
            _actions = new LinkedList();
        Action action = new Action();
        action.op = Action.GET_XPATH;
        action.data = meta;
        _actions.add(action);
        _cast = null;
        _key = false;
        _type = XPATH;
        _xmlfield = fmd;
    }

    @Override
    public void get(XMLMetaData meta, String name) {
        Action action = new Action();
        action.op = Action.GET_XPATH;
        action.data = meta.getFieldMapping(name);
        _actions.add(action);
        _cast = null;
        _key = false;
        _type = XPATH;
    }

    @Override
    public XMLMetaData getXmlMapping() {
        Action act = (Action) _actions.getLast();
        if (act != null)
            return (XMLMetaData) act.data;
        return null;
    }

    @Override
    public synchronized void getKey() {
        if (_cid)
            return;

        // replace the last field action to a get key
        Action action = lastFieldAction();
        Action key = new Action();
        key.op = Action.GET_KEY;
        key.data = action.data;
        int pos = _actions.indexOf(action);
        _actions.remove(action);
        _actions.add(pos, key);
        _cast = null;
        _key = true;
        _type = PATH;
    }

    private void checkObjectPathInheritanceTypeJoined(PathExpState pstate) {
        // if this mapping is in InheritanceType.JOINED,
        // then add joins
         ClassMapping base = _class;
         while (base.getJoinablePCSuperclassMapping() != null)
             base = base.getJoinablePCSuperclassMapping();
         if (base != _class) {
             ClassMapping from = _class;
             ClassMapping to = base;
             _type = OBJECT_PATH;
             for (; from != null && from != to; from = from.getJoinablePCSuperclassMapping()) {
                 pstate.joins = from.joinSuperclass(pstate.joins, false);
             }
         }
    }

    @Override
    public FieldMetaData last() {
        Action act = lastFieldAction();
        return (act == null) ? null : isXPath() ?
            _xmlfield : (FieldMetaData) act.data;
    }

    /**
     * Return the last action that gets a field.
     */
    private Action lastFieldAction() {
        if (_actions == null)
            return null;

        if (isXPath())
            return (Action) _actions.getLast();

        ListIterator itr = _actions.listIterator(_actions.size());
        Action prev;
        while (itr.hasPrevious()) {
            prev = (Action) itr.previous();
            if (prev.op == Action.GET || prev.op == Action.GET_OUTER
                || prev.op == Action.GET_KEY)
                return prev;
        }
        return null;
    }

    @Override
    public Class getType() {
        if (_cast != null)
            return _cast;
        Action act = lastFieldAction();
        if (act != null && act.op == Action.GET_XPATH)
            return ((XMLMetaData) act.data).getType();

        FieldMetaData fld = (act == null) ? null : (FieldMetaData) act.data;
        boolean key = act != null && act.op == Action.GET_KEY;
        if (fld != null) {
            switch (fld.getDeclaredTypeCode()) {
                case JavaTypes.ARRAY:
                    if (fld.getDeclaredType() == byte[].class
                        || fld.getDeclaredType() == Byte[].class
                        || fld.getDeclaredType() == char[].class
                        || fld.getDeclaredType() == Character[].class)
                        return fld.getDeclaredType();
                    return fld.getElement().getDeclaredType();
                case JavaTypes.MAP:
                    if (key)
                        return fld.getKey().getDeclaredType();
                    return fld.getElement().getDeclaredType();
                case JavaTypes.COLLECTION:
                    return fld.getElement().getDeclaredType();
                default:
                    return fld.getDeclaredType();
            }
        }
        if (_class != null)
            return _class.getDescribedType();
        return Object.class;
    }

    @Override
    public void setImplicitType(Class type) {
        _cast = type;
    }

    @Override
    public ExpState initialize(Select sel, ExpContext ctx, int flags) {
        PathExpState pstate = new PathExpState(sel.newJoins());
        boolean key = false;
        boolean forceOuter = false;
        ClassMapping rel = _candidate;

        sel.setSchemaAlias(_schemaAlias);

        // iterate to the final field
        ClassMapping owner;
        ClassMapping from, to;
        Action action;
        Variable var;
        Iterator itr = (_actions == null) ? null : _actions.iterator();
        FieldMapping field = null;
        Action prevaction = null;
        boolean isCorrelatedPath = false;
        boolean fromParentRootInSubselect = navigateFromParentRootInSubselect(sel);

        while (itr != null && itr.hasNext()) {
            action = (Action) itr.next();

            // treat subqueries like variables for alias generation purposes
            if (action.op == Action.VAR) {
                if (sel.getParent() != null && action.var != null &&
                    prevaction != null && prevaction.data != null &&
                    sel.ctx().getVariable(action.var) == null) {
                    isCorrelatedPath = true;
                    pstate.joins = pstate.joins.setCorrelatedVariable(action.var);
                } else
                    pstate.joins = pstate.joins.setVariable((String) action.data);
            }
            else if (action.op == Action.SUBQUERY) {
                pstate.joins = pstate.joins.setSubselect((String) action.data);
            }
            else if (action.op == Action.UNBOUND_VAR) {
                // unbound vars are cross-joined to the candidate table
                var = (Variable) action.data;
                rel = (ClassMapping) var.getMetaData();
                if (rel == null)
                	throw new IllegalArgumentException(_loc.get(
                	    "invalid-unbound-var", var.getName()).toString());

                if (sel.getParent() != null && action.var != null &&
                    sel.ctx().getVariable(action.var) == null) {
                    //System.out.println("Correlated action var="+action.var);
                    isCorrelatedPath = true;
                    pstate.joins = pstate.joins.setCorrelatedVariable(var.getName());
                } else
                    pstate.joins = pstate.joins.setVariable(var.getName());

                pstate.joins = pstate.joins.crossJoin(_candidate.getTable(),
                    rel.getTable());
                if (!itr.hasNext() && isVariable()) {
                    checkObjectPathInheritanceTypeJoined(pstate);
                }
            } else {
                // move past the previous field, if any
                field = (FieldMapping) ((action.op == Action.GET_XPATH) ?
                    _xmlfield : action.data);

                if (pstate.field != null) {
                    // if this is the second-to-last field and the last is
                    // the related field this field joins to, no need to
                    // traverse: just use this field's fk columns
                    if (!itr.hasNext() && (flags & JOIN_REL) == 0
                        && isJoinedField(pstate.field, key, field)) {
                        pstate.cmpfield = field;
                        break;
                    }

                    if (fromParentRootInSubselect) {
                        isCorrelatedPath = true;
                        pstate.joins = pstate.joins.setCorrelatedVariable(_schemaAlias);
                        pstate.joins.setJoinContext(null);
                    }

                    rel = traverseField(pstate, key, forceOuter ||
                              ctx.store.getDBDictionary().fullResultCollectionInOrderByRelation, false);
                }

                // mark if the next traversal should go through
                // the key rather than value
                key = action.op == Action.GET_KEY;
                forceOuter |= action.op == Action.GET_OUTER;

                if (key && itr.hasNext())
                    _keyPath = true;

                // get mapping for the current field
                pstate.field = field;

                owner = pstate.field.getDefiningMapping();
                if (pstate.field.getManagement()
                    != FieldMetaData.MANAGE_PERSISTENT)
                    throw new UserException(_loc.get("non-pers-field",
                        pstate.field));

                // find the most-derived type between the declared relation
                // type and the field's owner, and join from that type to
                // the lesser derived type
                if (rel != owner && rel != null) {
                    if (rel.getDescribedType().isAssignableFrom
                        (owner.getDescribedType())) {
                        from = owner;
                        to = rel;
                    } else {
                        from = rel;
                        to = owner;
                    }

                    for (; from != null && from != to;
                        from = from.getJoinablePCSuperclassMapping()) {
                    	FieldMapping cast = from.getFieldMapping(pstate.field
                    			.getName());
                    	if (cast != null)
                    		pstate.field = cast;
                        pstate.joins = from.joinSuperclass(pstate.joins, false);
                    }
                }
                // nothing more to do from here on as we encountered an xpath
                // action
                if (action.op == Action.GET_XPATH)
                    break;
            }
            prevaction = action;
            if (prevaction != null && prevaction.context != null) {
                Context jCtx = JDBCStoreQuery.getThreadLocalContext(prevaction.context);
                pstate.joins = pstate.joins.setJoinContext(jCtx);
            }
        }
        if (_varName != null)
            pstate.joins = pstate.joins.setVariable(_varName);

        // if we're not comparing to null or doing an isEmpty, then
        // join into the data on the final field; obviously we can't do these
        // joins when comparing to null b/c the whole purpose is to see
        // whether the joins even exist
        if ((flags & NULL_CMP) == 0)
            traverseField(pstate, key, forceOuter, true);
        pstate.joinedRel = false;
        if ((flags & JOIN_REL) != 0)
            joinRelation(pstate, key, forceOuter || (flags & FORCE_OUTER) != 0,
                false);
        if (isCorrelatedPath) {
            // check if there are joins that belong to parent
            pstate.joins.moveJoinsToParent();
        }
        pstate.joins.setJoinContext(null);

        if (_actions == null) {
            String subqAlias = findSubqAlias(sel);
            pstate.joins = pstate.joins.setSubselect(subqAlias);
            pstate.joins.setCorrelatedVariable(_schemaAlias);
            checkObjectPathInheritanceTypeJoined(pstate);
        }

        return pstate;
    }

    private String findSubqAlias(Select sel) {
        Select pSel = sel.getParent();
        if (pSel == null)
            return null;
        Context pCtx = pSel.ctx();
        if (pCtx.subquery == null)
            return null;
        if (pCtx.getSchema(_schemaAlias) != null)
            return ((SubQ)pCtx.subquery).getCandidateAlias();
        return findSubqAlias(pSel);
    }

    /**
     * When a PCPath is in subselect, and it is simply a navigation
     * from the parent root, the joins involved in this PCPath
     * must happen in the main select.
     */
    private boolean navigateFromParentRootInSubselect(Select sel) {
        if (sel.getParent() == null)
            return false;
        Iterator itr = (_actions == null) ? null : _actions.iterator();

        boolean hasVar = false;
        boolean startsWithSubquery = false;
        while (itr != null && itr.hasNext()) {
            Action action = (Action) itr.next();
            if (action.op == Action.VAR)
                hasVar = true;
            else if (action.op == Action.SUBQUERY)
                startsWithSubquery = true;
        }
        return !hasVar && !startsWithSubquery && sel.ctx().getSchema(_schemaAlias) == null;
    }

    /**
     * Return whether the given source field joins to the given target field.
     */
    private static boolean isJoinedField(FieldMapping src, boolean key,
        FieldMapping target) {
        ValueMapping vm;
        switch (src.getTypeCode()) {
            case JavaTypes.ARRAY:
            case JavaTypes.COLLECTION:
                vm = src.getElementMapping();
                break;
            case JavaTypes.MAP:
                vm = (key) ? src.getKeyMapping() : src.getElementMapping();
                break;
            default:
                vm = src;
        }
        if (vm.getJoinDirection() != ValueMapping.JOIN_FORWARD)
            return false;
        ForeignKey fk = vm.getForeignKey();
        if (fk == null)
            return false;

        // foreign key must join to target columns
        Column[] rels = fk.getColumns();
        Column[] pks = target.getColumns();
        if (rels.length != pks.length)
            return false;
        for (int i = 0; i < rels.length; i++)
            if (fk.getPrimaryKeyColumn(rels[i]) != pks[i])
                return false;
        return true;
    }

    @Override
    protected Object eval(Object candidate, Object orig,
        StoreContext ctx, Object[] params) {
        if (_actions == null)
            return candidate;

        Action action;
        OpenJPAStateManager sm;
        Broker tmpBroker;
        for (Object o : _actions) {
            action = (Action) o;
            sm = null;
            tmpBroker = null;
            if (ImplHelper.isManageable(candidate))
                sm = (OpenJPAStateManager) (ImplHelper.toPersistenceCapable(
                        candidate, ctx.getConfiguration())).
                        pcGetStateManager();
            if (sm == null) {
                tmpBroker = ctx.getBroker();
                tmpBroker.transactional(candidate, false, null);
                sm = tmpBroker.getStateManager(candidate);
            }
            if (action.op != Action.GET && action.op != Action.GET_OUTER)
                continue;
            try {
                candidate = sm.fetchField(
                        ((FieldMapping) action.data).getIndex(), true);
            }
            catch (ClassCastException cce) {
                throw new RuntimeException(action.data + " not a field path");
            }
            finally {
                // transactional does not clear the state, which is
                // important since tmpCandidate might be also managed by
                // another broker if it's a proxied non-pc instance
                if (tmpBroker != null)
                    tmpBroker.nontransactional(sm.getManagedInstance(), null);
            }
        }
        return candidate;
    }

    /**
     * Expression state.
     */
    public static class PathExpState
        extends ExpState {

        public FieldMapping field = null;
        public FieldMapping cmpfield = null;
        public Column[] cols = null;
        public boolean joinedRel = false;
        public boolean isEmbedElementColl = false;

        public PathExpState(Joins joins) {
            super(joins);
        }
    }

    /**
     * Traverse into the previous field of a relation path.
     *
     * @param last whether this is the last field in the path
     * @return the mapping of the related type, or null
     */
    private ClassMapping traverseField(PathExpState pstate, boolean key,
        boolean forceOuter, boolean last) {
        if (pstate.field == null)
            return null;

        // traverse into field value
        if (key)
            pstate.joins = pstate.field.joinKey(pstate.joins, forceOuter);
        else
            pstate.joins = pstate.field.join(pstate.joins, forceOuter);

        // if this isn't the last field, traverse into the relation
        if (!last)
            joinRelation(pstate, key, forceOuter, true);

        // return the maping of the related type, if any
        if (key)
            return pstate.field.getKeyMapping().getTypeMapping();
        if (pstate.field.getElement().getTypeCode() == JavaTypes.PC)
            return pstate.field.getElementMapping().getTypeMapping();
        return pstate.field.getTypeMapping();
    }

    /**
     * Join into the relation represented by the current field, if any.
     */
    private void joinRelation(PathExpState pstate, boolean key,
        boolean forceOuter, boolean traverse) {
        if (pstate.field == null)
            return;
        if (key)
            pstate.joins = pstate.field.joinKeyRelation(pstate.joins,
                forceOuter, traverse);
        else
            pstate.joins = pstate.field.joinRelation(pstate.joins, forceOuter,
                traverse);
        pstate.joinedRel = true;
    }

    @Override
    public Object toDataStoreValue(Select sel, ExpContext ctx, ExpState state,
        Object val) {
        PathExpState pstate = (PathExpState) state;
        FieldMapping field = (pstate.cmpfield != null) ? pstate.cmpfield
            : pstate.field;
        if (isXPath())
            return val;
        if (field != null) {
            if (_key)
                return field.toKeyDataStoreValue(val, ctx.store);
            if (field.getElement().getDeclaredTypeCode() != JavaTypes.OBJECT)
                return field.toDataStoreValue(val, ctx.store);

            val = field.getExternalValue(val, ctx.store.getContext());
            return field.toDataStoreValue(val, ctx.store);
        }
        return _class.toDataStoreValue(val, _class.getPrimaryKeyColumns(),
            ctx.store);
    }

    @Override
    public void select(Select sel, ExpContext ctx, ExpState state,
        boolean pks) {
        selectColumns(sel, ctx, state, pks);
    }

    @Override
    public void selectColumns(Select sel, ExpContext ctx, ExpState state,
        boolean pks) {
        sel.setSchemaAlias(_schemaAlias);
        ClassMapping mapping = getClassMapping(state);
        PathExpState pstate = (PathExpState) state;
        if (_type != OBJECT_PATH && (mapping == null || !pstate.joinedRel ||
            pstate.isEmbedElementColl))
            sel.select(getColumns(state), pstate.joins);
        else if (_key && pstate.field.getKey().isEmbedded())
            selectEmbeddedMapKey(sel, ctx, state);
        else if (pks)
            sel.select(mapping.getPrimaryKeyColumns(), pstate.joins);
        else {
            // select the mapping; allow any subs because we know this must
            // be either a relation, in which case it will already be
            // constrained by the joins, or 'this', in which case the
            // JDBCExpressionFactory takes care of adding class conditions for
            // the candidate class on the select
            int subs = (_type == UNBOUND_VAR) ? Select.SUBS_JOINABLE
                : Select.SUBS_ANY_JOINABLE;
            sel.select(mapping, subs, ctx.store, ctx.fetch,
                EagerFetchModes.EAGER_NONE, sel.outer(pstate.joins));
        }
    }

    @Override
    public void groupBy(Select sel, ExpContext ctx, ExpState state) {
        ClassMapping mapping = getClassMapping(state);
        PathExpState pstate = (PathExpState) state;
        if (mapping == null || !pstate.joinedRel)
            sel.groupBy(getColumns(state), sel.outer(pstate.joins));
        else {
            int subs = (_type == UNBOUND_VAR) ? Select.SUBS_JOINABLE
                : Select.SUBS_ANY_JOINABLE;
            sel.groupBy(mapping, subs, ctx.store, ctx.fetch,
                sel.outer(pstate.joins));
        }
    }

    @Override
    public void orderBy(Select sel, ExpContext ctx, ExpState state,
        boolean asc) {
        sel.orderBy(getColumns(state), asc, sel.outer(state.joins), false);
    }

    @Override
    public Object load(ExpContext ctx, ExpState state, Result res)
        throws SQLException {
        return load(ctx, state, res, false);
    }

    Object load(ExpContext ctx, ExpState state, Result res, boolean pks)
        throws SQLException {
        ClassMapping mapping = getClassMapping(state);
        PathExpState pstate = (PathExpState) state;
        if (mapping != null && (pstate.field == null
            || !pstate.field.isEmbedded())) {
            if (pks)
                return mapping.getObjectId(ctx.store, res, null, true,
                    pstate.joins);
            if (_key && pstate.field.getKey().isEmbedded())
                return loadEmbeddedMapKey(ctx, state, res);
            if (pstate.isEmbedElementColl)
                return pstate.field.loadProjection(ctx.store, ctx.fetch, res,
                    pstate.joins);
            return res.load(mapping, ctx.store, ctx.fetch, pstate.joins);
        }

        Object ret;
        if (_key) {
            // Map key is a java primitive type
            // example: Map<Integer, Employee> emps
            ret = res.getObject(pstate.cols[0], null, pstate.joins);
        } else {
            ret = pstate.field.loadProjection(ctx.store, ctx.fetch, res, pstate.joins);
        }

        if (pstate.field.isExternalized()) {
            ret = pstate.field.getFieldValue(ret, ctx.store.getContext());
        }

        if (_cast != null)
            ret = Filters.convert(ret, _cast);
        return ret;
    }

    private void validateMapStrategy(Strategy strategy) {
        if (strategy == null ||
            !(strategy instanceof LRSMapFieldStrategy))
            throw new RuntimeException("Invalid map field strategy:"+strategy);
    }

    private void selectEmbeddedMapKey(Select sel, ExpContext ctx,
        ExpState state) {
        PathExpState pstate = (PathExpState) state;
        validateMapStrategy(pstate.field.getStrategy());
        LRSMapFieldStrategy strategy = (LRSMapFieldStrategy)
            pstate.field.getStrategy();
        ClassMapping mapping = pstate.field.getKeyMapping().getTypeMapping();
        strategy.selectKey(sel, mapping, null, ctx.store, ctx.fetch,
            pstate.joins);
    }

    private Object loadEmbeddedMapKey(ExpContext ctx, ExpState state,
        Result res) throws SQLException {
        PathExpState pstate = (PathExpState) state;
        validateMapStrategy(pstate.field.getStrategy());
        LRSMapFieldStrategy strategy =
            (LRSMapFieldStrategy) pstate.field.getStrategy();
        return strategy.loadKey(null, ctx.store, ctx.fetch, res,
            pstate.joins);
    }

    @Override
    public void calculateValue(Select sel, ExpContext ctx, ExpState state,
        Val other, ExpState otherState) {
        // we don't create the SQL b/c it forces the Select to cache aliases
        // for the tables we use, and these aliases might not ever be used if
        // we eventually call appendIsEmpty or appendIsNull rather than appendTo
    }

    public void verifyIndexedField() {
        Action lastAction = (Action) lastFieldAction();
        FieldMapping fm = (FieldMapping) lastAction.data;
        if (fm.getOrderColumn() == null)
            throw new UserException(_loc.get("no-order-column", fm.getName()));
    }

    @Override
    public int length(Select sel, ExpContext ctx, ExpState state) {
        return getColumns(state).length;
    }

    public void appendTo(Select sel, ExpContext ctx, ExpState state,
        SQLBuffer sql) {
        Column[] cols = getColumns(state);
        for (int i = 0; i < cols.length; i++) {
            appendTo(sel, state, sql, cols[i]);
            if (i < cols.length -1)
            sql.append(", ");
        }
    }

    @Override
    public void appendTo(Select sel, ExpContext ctx, ExpState state,
        SQLBuffer sql, int index) {
        Column col = getColumns(state)[index];
        appendTo(sel, state, sql, col);
    }

    public void appendTo(Select sel, ExpState state, SQLBuffer sql, Column col) {
        if (sel != null)
            sel.setSchemaAlias(_schemaAlias);

        // if select is null, it means we are not aliasing columns
        // (e.g., during a bulk update)
        if (sel == null)
            sql.append(col.getIdentifier());
        else if (_type == XPATH)
            // if this is an xpath, append xpath string
            sql.append(getXPath());
        else
            sql.append(sel.getColumnAlias(col, state.joins));
    }

    @Override
    public void appendIsEmpty(Select sel, ExpContext ctx, ExpState state,
        SQLBuffer sql) {
        PathExpState pstate = (PathExpState) state;
        if (pstate.field == null)
            sql.append(FALSE);
        else
            pstate.field.appendIsEmpty(sql, sel, pstate.joins);
    }

    @Override
    public void appendIsNotEmpty(Select sel, ExpContext ctx, ExpState state,
        SQLBuffer sql) {
        PathExpState pstate = (PathExpState) state;
        if (pstate.field == null)
            sql.append(FALSE);
        else
            pstate.field.appendIsNotEmpty(sql, sel, pstate.joins);
    }

    @Override
    public void appendIndex(Select sel, ExpContext ctx, ExpState state,
        SQLBuffer sql) {
        PathExpState pstate = (PathExpState) state;
        if (pstate.field == null)
            sql.append("1");
        else
            pstate.field.appendIndex(sql, sel, pstate.joins);
    }

    @Override
    public void appendType(Select sel, ExpContext ctx, ExpState state,
        SQLBuffer sql) {
        Discriminator disc = null;
        ClassMapping sup = _class;
        while (sup.getMappedPCSuperclassMapping() != null)
            sup = sup.getMappedPCSuperclassMapping();

        disc = sup.getDiscriminator();

        Column[] cols = null;
        if (disc != null)
            cols = disc.getColumns();
        else
            cols = getColumns(state);

        if (cols == null || cols.length == 0) {
            sql.append("1");
            return;
        }

        for (int i = 0; i < cols.length; i++) {
            if (i > 0)
                sql.append(", ");
            sql.append(sel.getColumnAlias(cols[i], state.joins));
        }
    }

    @Override
    public void appendSize(Select sel, ExpContext ctx, ExpState state,
        SQLBuffer sql) {
        PathExpState pstate = (PathExpState) state;
        if (pstate.field == null)
            sql.append("1");
        else
            pstate.field.appendSize(sql, sel, pstate.joins);
    }

    @Override
    public void appendIsNull(Select sel, ExpContext ctx, ExpState state,
        SQLBuffer sql) {
        PathExpState pstate = (PathExpState) state;
        if (pstate.field == null)
            sql.append(FALSE);
        else
            pstate.field.appendIsNull(sql, sel, pstate.joins);
    }

    @Override
    public void appendIsNotNull(Select sel, ExpContext ctx, ExpState state,
        SQLBuffer sql) {
        PathExpState pstate = (PathExpState) state;
        if (pstate.field == null)
            sql.append(TRUE);
        else
            pstate.field.appendIsNotNull(sql, sel, pstate.joins);
    }

    public boolean isSubqueryPath() {
        if (_actions != null && _actions.size() == 1 &&
           ((Action)_actions.get(0)).op == Action.SUBQUERY)
            return true;
        return false;
    }

    @Override
    public int hashCode() {
        if (_actions == null)
            return _candidate.hashCode();
        return _candidate.hashCode() ^ _actions.hashCode();
    }

    @Override
    public boolean equals(Object other) {
        if (other == this)
            return true;
        if (!(other instanceof PCPath))
            return false;
        PCPath path = (PCPath) other;
        return Objects.equals(_candidate, path._candidate)
            && Objects.equals(_actions, path._actions);
    }

    @Override
    public int getId() {
        return Val.VAL;
    }


    /**
     * Helper class representing an action.
     */
    private static class Action
        implements Serializable {

        
        private static final long serialVersionUID = 1L;
        public static final int GET = 0;
        public static final int GET_OUTER = 1;
        public static final int GET_KEY = 2;
        public static final int VAR = 3;
        public static final int SUBQUERY = 4;
        public static final int UNBOUND_VAR = 5;
        public static final int CAST = 6;
        public static final int GET_XPATH = 7;

        public int op = -1;
        public Object data = null;
        public String var = null;
        public Context context = null;

        @Override
        public String toString() {
            return op + "|" + data;
        }

        @Override
        public int hashCode() {
            if (data == null)
                return op;
            return op ^ data.hashCode();
        }

        @Override
        public boolean equals(Object other) {
            if (other == null)
                return false;
            if (other == this)
                return true;
            Action a = (Action) other;
            return op == a.op
                && Objects.equals(data, a.data);
        }
    }
}
