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

import jakarta.persistence.criteria.Expression;
import jakarta.persistence.criteria.Join;
import jakarta.persistence.criteria.JoinType;
import jakarta.persistence.criteria.Path;
import jakarta.persistence.metamodel.Bindable;
import jakarta.persistence.metamodel.ManagedType;
import jakarta.persistence.metamodel.MapAttribute;
import jakarta.persistence.metamodel.PluralAttribute;
import jakarta.persistence.metamodel.SingularAttribute;
import jakarta.persistence.metamodel.Type;
import jakarta.persistence.metamodel.Type.PersistenceType;

import org.apache.openjpa.kernel.exps.ExpressionFactory;
import org.apache.openjpa.kernel.exps.Value;
import org.apache.openjpa.meta.ClassMetaData;
import org.apache.openjpa.meta.FieldMetaData;
import org.apache.openjpa.persistence.meta.Members;
import org.apache.openjpa.persistence.meta.Members.Member;

/**
 * Path is an expression representing a persistent attribute traversed from a parent path.
 * The type of the path is the type of the persistent attribute.
 * If the persistent attribute is bindable, then further path can be traversed from this path.
 *
 * @author Pinaki Poddar
 * @author Fay Wang
 *
 * @param <Z> the type of the parent path
 * @param <X> the type of this path
 */
class PathImpl<Z,X> extends ExpressionImpl<X> implements Path<X> {
    protected final PathImpl<?,Z> _parent;
    protected final Members.Member<? super Z,?> _member;
    private boolean isEmbedded = false;
    private PathImpl<?,?> _correlatedPath;

    /**
     * Protected constructor use by root path which neither represent a member nor has a parent.
     */
    protected PathImpl(Class<X> cls) {
        super(cls);
        _parent = null;
        _member = null;
    }

    /**
     * Create a path from the given parent representing the given member.
     *
     * @param parent the path from which this path needs to be constructed. Must not be null.
     * @param member the persistent property that represents this path.
     * @param cls denotes the type expressed by this path.
     */
    public PathImpl(PathImpl<?,Z> parent, Members.Member<? super Z, ?> member, Class<X> cls) {
        super(cls);
        _parent = parent;
        if (_parent.isEmbedded) {
            FieldMetaData fmd = getEmbeddedFieldMetaData(member.fmd);
            _member = new Members.SingularAttributeImpl(member.owner, fmd);
        } else {
            _member = member;
        }
        isEmbedded = _member.fmd.isElementCollection() ? _member.fmd.getElement().isEmbedded() :
            _member.fmd.isEmbedded();
    }

    /**
     * Gets the bindable object that corresponds to the path expression.
     *
     * @throws IllegalArgumentException if this path is not bindable
     */
    @Override
    public Bindable<X> getModel() {
        if (!(_member instanceof Bindable<?>)) {
            throw new IllegalArgumentException(this + " represents a basic path and not a bindable");
        }
        return (Bindable<X>)_member;
    }

    /**
     *  Gets the parent of this path or null if this path is the root.
     */
    @Override
    public final Path<Z> getParentPath() {
        return _parent;
    }

    /**
     * Gets the path that originates this traversal. Can be itself if this path is the root.
     */
    public PathImpl<?,?> getInnermostParentPath() {
        return (_parent == null) ? this : _parent.getInnermostParentPath();
    }

    /**
     * Gets the field that may have been embedded inside the given field.
     * For example, a given primary key field which is using an embedded class as a complex primary key.
     * @param fmd a given field
     * @return the embedded field or the given field itself
     */
    protected FieldMetaData getEmbeddedFieldMetaData(FieldMetaData fmd) {
        Members.Member<?,?> member = getInnermostMember(_parent,_member);

        ClassMetaData embeddedMeta = member.fmd.isElementCollection()
        		? member.fmd.getElement().getEmbeddedMetaData()
        		: member.fmd.getEmbeddedMetaData();

        return (embeddedMeta != null) ? embeddedMeta.getField(fmd.getName()) : fmd;
    }

    protected Members.Member<?,?> getInnermostMember(PathImpl<?,?> parent, Members.Member<?,?> member) {
        return member != null ? member : getInnermostMember(parent._parent,  parent._member);
    }

    /**
     * Makes this path correlated to the given path.
     */
    public void setCorrelatedPath(PathImpl<?,?> correlatedPath) {
        _correlatedPath = correlatedPath;
    }

    /**
     * Gets the path correlated to this path, if any.
     */
    public PathImpl<?,?> getCorrelatedPath() {
        return _correlatedPath;
    }

    /**
     * Affirms if this path is correlated to another path.
     */
    public boolean isCorrelated() {
        return _correlatedPath != null;
    }

    /**
     * Convert this path to a kernel path.
     */
    @Override
    public Value toValue(ExpressionFactory factory, CriteriaQueryImpl<?> q) {
        if (q.isRegistered(this))
            return q.getRegisteredValue(this);
        org.apache.openjpa.kernel.exps.Path path = null;
        SubqueryImpl<?> subquery = q.getDelegator();
        boolean allowNull = _parent == null ? false : _parent instanceof Join
            && ((Join<?,?>)_parent).getJoinType() != JoinType.INNER;
        PathImpl<?,?> corrJoin = getCorrelatedJoin(this);
        PathImpl<?,?> corrRoot = getCorrelatedRoot(subquery);
        if (_parent != null && q.isRegistered(_parent)) {
            path = factory.newPath(q.getRegisteredVariable(_parent));
            path.setSchemaAlias(q.getAlias(_parent));
            path.get(_member.fmd, allowNull);
        } else if (_parent != null && _parent._correlatedPath != null && q.isRegistered(_parent._correlatedPath)){
            path = factory.newPath(q.getRegisteredVariable(_parent._correlatedPath));
            path.setSchemaAlias(q.getAlias(_parent._correlatedPath));
            path.get(_member.fmd, allowNull);
        } else if (corrJoin != null || corrRoot != null) {
            org.apache.openjpa.kernel.exps.Subquery subQ = subquery.getSubQ();
            path = factory.newPath(subQ);
            path.setMetaData(subQ.getMetaData());
            path.setSchemaAlias(q.getAlias(_parent));
            traversePath(_parent, path, _member.fmd);
        } else if (_parent != null) {
            Value val = _parent.toValue(factory, q);
            if (val instanceof org.apache.openjpa.kernel.exps.Path) {
                path = (org.apache.openjpa.kernel.exps.Path)val;
                path.get(_member.fmd, allowNull);
            } else {
                val.setAlias(q.getAlias(this));
                return val;
            }
        } else if (_parent == null) {
            path = factory.newPath();
            path.setMetaData(q.getMetamodel().getRepository().getCachedMetaData(getJavaType()));
        }
        if (_member != null && !_member.isCollection()) {
            path.setImplicitType(getJavaType());
        }
        path.setAlias(q.getAlias(this));
        return path;
    }

    public PathImpl<?,?> getCorrelatedRoot(SubqueryImpl<?> subquery) {
        if (subquery == null)
            return null;
        PathImpl<?,?> root = getInnermostParentPath();
        if (subquery.getRoots() != null && subquery.getRoots().contains(this))
            return root;
        return null;
    }


    public PathImpl<?,?> getCorrelatedJoin(PathImpl<?,?> path) {
        if (path._correlatedPath != null)
            return path._correlatedPath;
        if (path._parent == null)
            return null;
        return getCorrelatedJoin(path._parent);
    }

    /**
     * Affirms if this receiver occurs in the roots of the given subquery.
     */
    public boolean inSubquery(SubqueryImpl<?> subquery) {
        return subquery != null && (subquery.getRoots() == null ? false : subquery.getRoots().contains(this));
    }

    protected void traversePath(PathImpl<?,?> parent,  org.apache.openjpa.kernel.exps.Path path, FieldMetaData fmd) {
        boolean allowNull = parent == null ? false : parent instanceof Join
            && ((Join<?,?>)parent).getJoinType() != JoinType.INNER;
        FieldMetaData fmd1 = parent._member == null ? null : parent._member.fmd;
        PathImpl<?,?> parent1 = parent._parent;
        if (parent1 == null || parent1.getCorrelatedPath() != null) {
            if (fmd != null)
                path.get(fmd, allowNull);
            return;
        }
        traversePath(parent1, path, fmd1);
        if (fmd != null)
            path.get(fmd, allowNull);
    }

    /**
     *  Gets a new path that represents the given single-valued attribute from this path.
     */
    @Override
    public <Y> Path<Y> get(SingularAttribute<? super X, Y> attr) {
    	if (getType() != attr.getDeclaringType()) {
    		attr = (SingularAttribute)((ManagedType)getType()).getAttribute(attr.getName());
    	}
        return new PathImpl<>(this, (Members.SingularAttributeImpl<? super X, Y>)attr, attr.getJavaType());
    }

    /**
     *  Gets a new path that represents the given multi-valued attribute from this path.
     */
    @Override
    public <E, C extends java.util.Collection<E>> Expression<C>  get(PluralAttribute<X, C, E> coll) {
    	if (getType() != coll.getDeclaringType()) {
    		coll = (PluralAttribute)((ManagedType)getType()).getAttribute(coll.getName());
    	}
        return new PathImpl<>(this, (Members.PluralAttributeImpl<? super X, C, E>)coll, coll.getJavaType());
    }

    /**
     *  Gets a new path that represents the given map-valued attribute from this path.
     */
    @Override
    public <K, V, M extends java.util.Map<K, V>> Expression<M> get(MapAttribute<X, K, V> map) {
    	if (getType() != map.getDeclaringType()) {
    		map = (MapAttribute)((ManagedType)getType()).getAttribute(map.getName());
    	}
        return new PathImpl<>(this, (Members.MapAttributeImpl<? super X,K,V>)map, (Class<M>)map.getJavaType());
    }

    /**
     * Gets a new path that represents the attribute of the given name from this path.
     *
     * @exception IllegalArgumentException if this path represents a basic attribute that is can not be traversed
     * further.
     */
    @Override
    public <Y> Path<Y> get(String attName) {
        Type<?> type = this.getType();
        if (type.getPersistenceType() == PersistenceType.BASIC) {
            throw new IllegalArgumentException(this + " is a basic path and can not be navigated to " + attName);
        }

        Members.Member<? super X, Y> next = (Members.Member<? super X, Y>)
           ((ManagedType<? super X>)type).getAttribute(attName);
        return new PathImpl<>(this, next, next.getJavaType());
    }

    public Type<?> getType() {
        return _member.getType();
    }

    @SuppressWarnings("unchecked")
    public Member<? extends Z, X> getMember() {
        return (Member<? extends Z, X>) _member;
    }

    /**
     * Get the type() expression corresponding to this path.
     */
    @Override
    public Expression<Class<? extends X>> type() {
        return new Expressions.Type<>(this);
    }

    @Override
    public StringBuilder asValue(AliasContext q) {
        StringBuilder buffer = new StringBuilder();
        if (_parent != null) {
            Value var = q.getRegisteredVariable(_parent);
            buffer.append(var != null ? var.getName() : _parent.asValue(q)).append(".");
        }
        if (_member != null) {
            buffer.append(_member.fmd.getName());
        }
        return buffer;
    }

    @Override
    public StringBuilder asVariable(AliasContext q) {
        Value var = q.getRegisteredVariable(this);
        return asValue(q).append(" ").append(var == null ? "?" : var.getName());
    }
}
