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

import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;

import org.apache.openjpa.enhance.PCRegistry;
import org.apache.openjpa.kernel.Query;
import org.apache.openjpa.kernel.QueryContext;
import org.apache.openjpa.kernel.StoreContext;
import org.apache.openjpa.meta.ClassMetaData;
import org.apache.openjpa.meta.JavaTypes;
import org.apache.openjpa.meta.MetaDataRepository;
import org.apache.openjpa.util.ImplHelper;

/**
 * This class stores information about a particular invocation of
 * a query. It contains a reference to the external properties of the
 * query that was executed, as well as any parameters used to execute
 * that query, with one exception: first-class objects used as
 * parameter values are converted to OIDs.
 *
 * @author Patrick Linskey
 */
public class QueryKey
    implements Externalizable {

    // initialize the set of unmodifiable classes. This allows us
    // to avoid cloning collections that are not modifiable,
    // provided that they do not contain mutable objects.
    private static Collection<Class<?>> s_unmod = new HashSet<>();

    static {
        // handle the set types; jdk uses different classes for collection,
        // set, and sorted set
        TreeSet<Object> s = new TreeSet<>();
        s_unmod.add(Collections.unmodifiableCollection(s).getClass());
        s_unmod.add(Collections.unmodifiableSet(s).getClass());
        s_unmod.add(Collections.unmodifiableSortedSet(s).getClass());

        // handle the list types; jdk uses different classes for standard
        // and random access lists
        List<Object> l = new LinkedList<>();
        s_unmod.add(Collections.unmodifiableList(l).getClass());
        l = new ArrayList<>(0);
        s_unmod.add(Collections.unmodifiableList(l).getClass());

        // handle the constant types
        s_unmod.add(Collections.EMPTY_SET.getClass());
        s_unmod.add(Collections.EMPTY_LIST.getClass());
    }

    // caching state; no need to include parameter and variable declarations
    // because they are implicit in the filter
    private String _candidateClassName;
    private boolean _subclasses;
    private Set<String> _accessPathClassNames;
    private Object _query;
    private boolean _ignoreChanges;
    private Map<Object,Object> _params;
    private long _rangeStart;
    private long _rangeEnd;

    // ### pcl: 2 May 2003: should this timeout take into account the
    // ### timeouts for classes in the access path of the query?
    // ### Currently, it only considers the candidate class and its
    // ### subclasses. Note that this value is used to decide whether
    // ### or not OIDs should be registered for expiration callbacks
    private int _timeout = -1;

    /**
     * Return a key for the given query, or null if it is not cacheable.
     */
    public static QueryKey newInstance(Query q) {
        return newInstance(q, (Object[]) null);
    }

    /**
     * Return a key for the given query, or null if it is not cacheable.
     */
    public static QueryKey newInstance(Query q, Object[] args) {
        // compile to make sure info encoded in query string is available
        // via API calls (candidate class, result class, etc)
        q.compile();
        return newInstance(q, false, args, q.getCandidateType(),
            q.hasSubclasses(), q.getStartRange(), q.getEndRange(), null);
    }

    /**
     * Return a key for the given query, or null if it is not cacheable.
     */
    public static QueryKey newInstance(Query q, Map<Object,Object> args) {
        // compile to make sure info encoded in query string is available
        // via API calls (candidate class, result class, etc)
        q.compile();
        return newInstance(q, false, args, q.getCandidateType(),
            q.hasSubclasses(), q.getStartRange(), q.getEndRange(), null);
    }

    /**
     * Return a key for the given query, or null if it is not cacheable.
     */
    static QueryKey newInstance(QueryContext q, boolean packed, Object[] args,
        Class<?> candidate, boolean subs, long startIdx, long endIdx, Object parsed) {
        QueryKey key = createKey(q, packed, candidate, subs, startIdx, endIdx, parsed);
        if (key != null && setParams(key, q, args))
            return key;
        return null;
    }

    /**
     * Return a key for the given query, or null if it is not cacheable.
     */
    static QueryKey newInstance(QueryContext q, boolean packed, Map<Object,Object> args,
        Class<?> candidate, boolean subs, long startIdx, long endIdx, Object parsed) {
        QueryKey key = createKey(q, packed, candidate, subs, startIdx, endIdx, parsed);
        if (key != null && (args == null || args.isEmpty() ||
            setParams(key, q.getStoreContext(), new HashMap<>(args))))
            return key;
        return null;
    }

    /**
     * Extract the relevant identifying information from
     * <code>q</code>. This includes information such as candidate
     * class, query filter, etc.
     */
    private static QueryKey createKey(QueryContext q, boolean packed,
        Class<?> candidateClass, boolean subclasses, long startIdx, long endIdx, Object parsed) {
        if (candidateClass == null)
            return null;

        // can only cache datastore queries
        if (q.getCandidateCollection() != null)
            return null;

        // no support already-packed results
        if (q.getResultType() != null && packed)
            return null;

        // can't cache non-serializable non-managed complex types
        Class<?>[] types = q.getProjectionTypes();
        for (Class<?> type : types) {
            switch (JavaTypes.getTypeCode(type)) {
                case JavaTypes.ARRAY:
                    return null;
                case JavaTypes.COLLECTION:
                case JavaTypes.MAP:
                case JavaTypes.OBJECT:
                    if (!ImplHelper.isManagedType(
                            q.getStoreContext().getConfiguration(), type))
                        return null;
                    break;
            }
        }

        // we can't cache the query if we don't know which classes are in the
        // access path
        ClassMetaData[] metas = q.getAccessPathMetaDatas();
        if (metas.length == 0)
            return null;

        Set<String> accessPathClassNames = new HashSet<>((int) (metas.length * 1.33 + 1));
        ClassMetaData meta;
        for (ClassMetaData metaData : metas) {
            // since the class change framework deals with least-derived types,
            // record the least-derived access path types
            meta = metaData;
            accessPathClassNames.add(meta.getDescribedType().getName());
            while (meta.getPCSuperclass() != null) {
                meta = meta.getPCSuperclassMetaData();
            }

            accessPathClassNames.add(meta.getDescribedType().getName());
        }

        // if any of the types are currently dirty, we can't cache this query
        StoreContext ctx = q.getStoreContext();
        if (intersects(accessPathClassNames, ctx.getPersistedTypes())
            || intersects(accessPathClassNames, ctx.getUpdatedTypes())
            || intersects(accessPathClassNames, ctx.getDeletedTypes()))
            return null;

        // calculate the timeout for the key
        MetaDataRepository repos = ctx.getConfiguration().
            getMetaDataRepositoryInstance();

        // won't find metadata for interfaces.
        if (candidateClass.isInterface())
            return null;
        meta = repos.getMetaData(candidateClass, ctx.getClassLoader(), true);
        int timeout = meta.getDataCacheTimeout();
        if (subclasses) {
            metas = meta.getPCSubclassMetaDatas();
            int subTimeout;
            for (ClassMetaData classMetaData : metas) {
                if (classMetaData.getDataCache() == null)
                    return null;

                accessPathClassNames.add(classMetaData.getDescribedType().getName());
                subTimeout = classMetaData.getDataCacheTimeout();
                if (subTimeout != -1 && subTimeout < timeout)
                    timeout = subTimeout;
            }
        }

        // tests all passed; cacheable
        QueryKey key = new QueryKey();
        key._candidateClassName = candidateClass.getName();
        key._subclasses = subclasses;
        key._accessPathClassNames = accessPathClassNames;
        key._timeout = timeout;
        key._query = q.getQueryString();
        if (key._query == null && parsed != null) {
            // this is a criteria query. Store the Stringified query value rather than the full cq.
            key._query = parsed.toString();
        }
        key._ignoreChanges = q.getIgnoreChanges();
        key._rangeStart = startIdx;
        key._rangeEnd = endIdx;
        return key;
    }

    /**
     * Convert an array of arguments into the corresponding parameter
     * map, and do any PC to OID conversion necessary.
     */
    private static boolean setParams(QueryKey key, QueryContext q,
        Object[] args) {
        if (args == null || args.length == 0)
            return true;

        // Create a map for the given parameters, and convert the
        // parameter list into a map, using the query's parameter
        // declaration to determine ordering etc.
        Map<Object,Class<?>> types = q.getOrderedParameterTypes();
        Map<Object,Object> map = new HashMap<>((int) (types.size() * 1.33 + 1));
        int idx = 0;
        for (Iterator<Object> iter = types.keySet().iterator(); iter.hasNext(); idx++)
            map.put(iter.next(), args[idx]);
        return setParams(key, q.getStoreContext(), map);
    }

    /**
     * Convert parameters to a form that is cacheable. Mutable params
     * will be cloned.
     */
    private static boolean setParams(QueryKey key, StoreContext ctx,
        Map<Object,Object> params) {
        if (params == null || params.isEmpty())
            return true;

        Object v;
        for (Map.Entry<Object,Object> e : params.entrySet()) {
            v = e.getValue();
            if (ImplHelper.isManageable(v)) {
                if (!ctx.isPersistent(v) || ctx.isNew(v) || ctx.isDeleted(v))
                    return false;
                e.setValue(ctx.getObjectId(v));
            }

            if (v instanceof Collection) {
                Collection<Object> c = (Collection<Object>) v;
                boolean contentsAreDates = false;
                if (c.iterator().hasNext()) {
                    // this assumes that the collection is homogeneous
                    Object o = c.iterator().next();
                    if (ImplHelper.isManageable(o))
                        return false;

                    // pcl: 27 Jun 2004: if we grow this logic to
                    // handle other mutable types that are not
                    // known to be cloneable, we will have to add
                    // logic to handle them. This is because we
                    // can't just cast to Cloneable and invoke
                    // clone(), as clone() is a protected method
                    // in Object.
                    if (o instanceof Date)
                        contentsAreDates = true;

                    // if the collection is not a known immutable
                    // type, or if it contains mutable instances,
                    // clone it for good measure.
                    if (contentsAreDates || !s_unmod.contains(c.getClass())) {
                        // copy the collection
                        Collection<Object> copy;
                        if (c instanceof SortedSet)
                            copy = new TreeSet<>();
                        else if (c instanceof Set)
                            copy = new HashSet<>();
                        else
                            copy = new ArrayList<>(c.size());

                        if (contentsAreDates) {
                            // must go through by hand and do the
                            // copy, since Date is mutable.
                            for (Object value : c) {
                                copy.add(((Date) value).clone());
                            }
                        } else
                            copy.addAll(c);

                        e.setValue(copy);
                    }
                }
            } else if (v instanceof Date)
                e.setValue(((Date) v).clone());
        }

        key._params = params;
        return true;
    }

    /**
     * Public constructor for externalization only.
     */
    public QueryKey() {
    }

    /**
     * Returns the candidate class name for this query.
     */
    public String getCandidateTypeName() {
        return _candidateClassName;
    }

    /**
     * Return the amount of time this key is good for.
     */
    public int getTimeout() {
        return _timeout;
    }

    /**
     * Returns <code>true</code> if modifications to any of the
     * classes in <code>changed</code> results in a possible
     * invalidation of this query; otherwise returns
     * <code>false</code>. Invalidation is possible if one or more of
     * the classes in this query key's access path has been changed.
     */
    public boolean changeInvalidatesQuery(Collection<Class<?>> changed) {
        return intersects(_accessPathClassNames, changed);
    }

    /**
     * Whether the given set of least-derived class names intersects with
     * the given set of changed classes.
     */
    private static boolean intersects(Collection<String> names, Collection<Class<?>> changed) {
        Class<?> sup;
        for (Class<?> cls : changed) {
            while ((sup = PCRegistry.getPersistentSuperclass(cls)) != null)
                cls = sup;
            if (names.contains(cls.getName()))
                return true;
        }
        return false;
    }

    /**
     * Determine equality based on identifying information. Keys
     * created for queries that specify a candidate collection are
     * always not equal.
     */
    @Override
    public boolean equals(Object ob) {
        if (this == ob)
            return true;
        if (ob == null || getClass() != ob.getClass())
            return false;

        QueryKey other = (QueryKey) ob;
        return Objects.equals(_candidateClassName,
            other._candidateClassName)
            && _subclasses == other._subclasses
            && _ignoreChanges == other._ignoreChanges
            && _rangeStart == other._rangeStart
            && _rangeEnd == other._rangeEnd
            && Objects.equals(_query, other._query)
            && Objects.equals(_params, other._params);
    }

    /**
     * Define a hashing algorithm corresponding to the {@link #equals}
     * method defined above.
     */
    @Override
    public int hashCode() {
        int code = 37 * 17 + _candidateClassName.hashCode();
        if (_query != null)
            code = 37 * code + _query.hashCode();
        if (_params != null)
            code = 37 * code + _params.hashCode();
        return code;
    }

    @Override
    public String toString() {
        StringBuilder buf = new StringBuilder(1024);
        buf.append(super.toString()).
            append("[query:[").append(_query).append("]").
            append(",access path:").append(_accessPathClassNames).
            append(",subs:").append(_subclasses).
            append(",ignoreChanges:").append(_ignoreChanges).
            append(",startRange:").append(_rangeStart).
            append(",endRange:").append(_rangeEnd).
            append(",timeout:").append(_timeout).
            append("]");
        return buf.toString();
    }

    // ---------- Externalizable implementation ----------

    @Override
    public void writeExternal(ObjectOutput out)
        throws IOException {
        out.writeObject(_candidateClassName);
        out.writeBoolean(_subclasses);
        out.writeObject(_accessPathClassNames);
        out.writeObject(_query);
        out.writeBoolean(_ignoreChanges);
        out.writeObject(_params);
        out.writeLong(_rangeStart);
        out.writeLong(_rangeEnd);
        out.writeInt(_timeout);
    }

    @Override
    public void readExternal(ObjectInput in)
        throws IOException, ClassNotFoundException {
        _candidateClassName = (String) in.readObject();
        _subclasses = in.readBoolean();
        _accessPathClassNames = (Set<String>) in.readObject();
        _query = (String) in.readObject();
        _ignoreChanges = in.readBoolean();
        _params = (Map<Object,Object>) in.readObject();
        _rangeStart = in.readLong();
        _rangeEnd = in.readLong ();
		_timeout = in.readInt ();
	}

    /**
     * Returns the set of the accessPathClassnames that exists in the query
     * @return -- Returns a set of accesspath classnames.
     */
    public Set<String> getAcessPathClassNames() {
        return this._accessPathClassNames;
    }
}
