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

import java.util.LinkedList;
import java.util.ListIterator;
import java.util.Objects;

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.meta.ClassMetaData;
import org.apache.openjpa.meta.FieldMetaData;
import org.apache.openjpa.meta.XMLMetaData;
import org.apache.openjpa.util.ImplHelper;

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

    
    private static final long serialVersionUID = 1L;
    protected LinkedList _actions = null;
    protected String _correlationVar = null;

    /**
     * Traverse into the given field of the current object, and update
     * the current object to that field value.
     */
    @Override
    public void get(FieldMetaData field, boolean nullTraversal) {
        if (_actions == null)
            _actions = new LinkedList();
        _actions.add(new Traversal(field, nullTraversal));
    }

    @Override
    public Class getType() {
        if (_actions == null)
            return getCandidateType();

        Object last = _actions.getLast();
        if (last instanceof Class)
            return (Class) last;
        FieldMetaData fmd = ((Traversal) last).field;
        return fmd.getDeclaredType();
    }

    protected Class getCandidateType() {
        ClassMetaData meta = getMetaData();
        if (meta == null)
            return Object.class;
        return meta.getDescribedType();
    }

    @Override
    public void setImplicitType(Class type) {
    }

    @Override
    public FieldMetaData last() {
        if (_actions == null)
            return null;

        ListIterator itr = _actions.listIterator(_actions.size());
        Object prev;
        while (itr.hasPrevious()) {
            prev = itr.previous();
            if (prev instanceof Traversal)
                return ((Traversal) prev).field;
        }
        return null;
    }

    /**
     * Cast this path to the given type.
     */
    public void castTo(Class type) {
        if (_actions == null)
            _actions = new LinkedList();
        _actions.add(type);
    }

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

        Object action;
        OpenJPAStateManager sm;
        Broker tmpBroker = null;
        for (Object o : _actions) {
            action = o;

            // fail on null value
            if (candidate == null) {
                if (action instanceof Traversal
                        && ((Traversal) action).nullTraversal)
                    return null;
                throw new NullPointerException();
            }

            // check that the cast is valid
            if (action instanceof Class) {
                candidate = Filters.convert(candidate, (Class) action);
                continue;
            }

            // make sure we can access the instance; even non-pc vals might
            // be proxyable
            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);
            }

            try {
                // get the specified field value and switch candidate
                Traversal traversal = (Traversal) action;
                candidate = sm.fetchField(traversal.field.getIndex(), true);
            }
            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;
    }

    @Override
    public int hashCode() {
        return (_actions == null) ? 0 : _actions.hashCode();
    }

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

    /**
     * Represents a traversal through a field.
     */
    public static class Traversal {

        public final FieldMetaData field;
        public final boolean nullTraversal;

        private Traversal(FieldMetaData field, boolean nullTraversal) {
            this.field = field;
            this.nullTraversal = nullTraversal;
        }

        @Override
        public int hashCode() {
            return field.hashCode();
        }

        @Override
        public boolean equals(Object other) {
            if (other == this)
                return true;
            return ((Traversal) other).field.equals(field);
        }
	}

    @Override
    public void get(FieldMetaData fmd, XMLMetaData meta) {
    }

    @Override
    public void get(XMLMetaData meta, String name) {
    }

    @Override
    public XMLMetaData getXmlMapping() {
        return null;
    }

    @Override
    public void setSchemaAlias(String schemaAlias) {
    }

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

    @Override
    public void setSubqueryContext(Context conext, String correlationVar) {
    }

    @Override
    public String getCorrelationVar() {
        return _correlationVar;
    }
}
