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

import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.apache.openjpa.lib.rop.ListResultObjectProvider;
import org.apache.openjpa.lib.rop.RangeResultObjectProvider;
import org.apache.openjpa.lib.rop.ResultObjectProvider;
import org.apache.openjpa.lib.util.Localizer;
import org.apache.openjpa.lib.util.OrderedMap;
import org.apache.openjpa.lib.util.StringUtil;
import org.apache.openjpa.meta.ClassMetaData;
import org.apache.openjpa.util.Exceptions;
import org.apache.openjpa.util.ImplHelper;
import org.apache.openjpa.util.OpenJPAException;
import org.apache.openjpa.util.UserException;

/**
 * A query that is executed by a user-defined method.
 *
 * @author Abe White
 */
public class MethodStoreQuery
    extends AbstractStoreQuery {

    
    private static final long serialVersionUID = 1L;

    public static final String LANGUAGE = QueryLanguages.LANG_METHODQL;

    private static final Class[] ARGS_DATASTORE = new Class[]{
        StoreContext.class, ClassMetaData.class, boolean.class, Map.class,
        FetchConfiguration.class
    };
    private static final Class[] ARGS_INMEM = new Class[]{
        StoreContext.class, ClassMetaData.class, boolean.class,
        Object.class, Map.class, FetchConfiguration.class
    };
    private static final int OBJ_INDEX = 3;

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

    private OrderedMap<Object, Class<?>> _params = null;

    @Override
    public void invalidateCompilation() {
        if (_params != null)
            _params.clear();
    }

    @Override
    public Executor newInMemoryExecutor(ClassMetaData meta, boolean subs) {
        return new MethodExecutor(this, meta, subs, true);
    }

    @Override
    public Executor newDataStoreExecutor(ClassMetaData meta, boolean subs) {
        return new MethodExecutor(this, meta, subs, false);
    }

    @Override
    public boolean supportsInMemoryExecution() {
        return true;
    }

    @Override
    public boolean supportsDataStoreExecution() {
        return true;
    }

    @Override
    public boolean requiresCandidateType() {
        return false;
    }

    /**
     * Parse the parameter declarations.
     */
    private OrderedMap<Object, Class<?>> bindParameterTypes() {
        ctx.lock();
        try {
            if (_params != null)
                return _params;
            String params = ctx.getParameterDeclaration();
            if (params == null)
                return EMPTY_ORDERED_PARAMS;

            List decs = Filters.parseDeclaration(params, ',', "parameters");
            if (_params == null)
                _params = new OrderedMap<>();
            String name;
            Class cls;
            for (int i = 0; i < decs.size(); i += 2) {
                name = (String) decs.get(i);
                cls = ctx.classForName(name, null);
                if (cls == null)
                    throw new UserException(_loc.get("bad-param-type", name));
                _params.put(decs.get(i + 1), cls);
            }
            return _params;
        } finally {
            ctx.unlock();
        }
    }

    /**
     * Uses a user-defined method named by the filter string to execute the
     * query.
     */
    private static class MethodExecutor
        extends AbstractExecutor
        implements Executor {

        private final ClassMetaData _meta;
        private final boolean _subs;
        private final boolean _inMem;
        private Method _meth = null;

        public MethodExecutor(MethodStoreQuery q, ClassMetaData candidate,
            boolean subclasses, boolean inMem) {
            _meta = candidate;
            _subs = subclasses;
            _inMem = inMem;
        }

        @Override
        public ResultObjectProvider executeQuery(StoreQuery q,
            Object[] params, Range range) {
            // convert the parameters into a map
            Map paramMap;
            if (params.length == 0)
                paramMap = Collections.EMPTY_MAP;
            else {
                Map paramTypes = q.getContext().getOrderedParameterTypes();
                paramMap = new HashMap((int) (params.length * 1.33 + 1));
                int idx = 0;
                for (Iterator itr = paramTypes.keySet().iterator();
                    itr.hasNext(); idx++)
                    paramMap.put(itr.next(), params[idx]);
            }

            FetchConfiguration fetch = q.getContext().getFetchConfiguration();
            StoreContext sctx = q.getContext().getStoreContext();
            ResultObjectProvider rop;
            Object[] args;
            if (_inMem) {
                args = new Object[]{ sctx, _meta, (_subs) ? Boolean.TRUE
                    : Boolean.FALSE, null, paramMap, fetch };

                Iterator itr = null;
                Collection coll = q.getContext().getCandidateCollection();
                if (coll == null) {
                    Extent ext = q.getContext().getQuery().
                        getCandidateExtent();
                    itr = ext.iterator();
                } else
                    itr = coll.iterator();

                List results = new ArrayList();
                try {
                    Object obj;
                    while (itr.hasNext()) {
                        obj = itr.next();
                        if (obj == null
                            || !_meta.getDescribedType().isInstance(obj))
                            continue;

                        args[OBJ_INDEX] = obj;
                        if ((Boolean) invoke(q, args))
                            results.add(obj);
                    }
                }
                finally {
                    ImplHelper.close(itr);
                }
                rop = new ListResultObjectProvider(results);
            } else {
                // datastore
                args = new Object[]{ sctx, _meta, (_subs) ? Boolean.TRUE
                    : Boolean.FALSE, paramMap, fetch };
                rop = (ResultObjectProvider) invoke(q, args);
            }

            if (range.start != 0 || range.end != Long.MAX_VALUE)
                rop = new RangeResultObjectProvider(rop, range.start,range.end);
            return rop;
        }

        /**
         * Invoke the internal method with the given arguments, returning the
         * result.
         */
        private Object invoke(StoreQuery q, Object[] args) {
            validate(q);
            try {
                return _meth.invoke(null, args);
            } catch (OpenJPAException ke) {
                throw ke;
            } catch (Exception e) {
                throw new UserException(_loc.get("method-error", _meth,
                    Exceptions.toString(Arrays.asList(args))), e);
            }
        }

        @Override
        public void validate(StoreQuery q) {
            if (_meth != null)
                return;

            String methName = q.getContext().getQueryString();
            if (StringUtil.isEmpty(methName))
                throw new UserException(_loc.get("no-method"));

            int dotIdx = methName.lastIndexOf('.');
            Class cls;
            if (dotIdx == -1)
                cls = _meta.getDescribedType();
            else {
                cls = q.getContext().classForName(methName.substring(0, dotIdx),
                    null);
                if (cls == null)
                    throw new UserException(_loc.get("bad-method-class",
                        methName.substring(0, dotIdx), methName));
                methName = methName.substring(dotIdx + 1);
            }

            Method meth;
            Class[] types = (_inMem) ? ARGS_INMEM : ARGS_DATASTORE;
            try {
                meth = cls.getMethod(methName, types);
            } catch (Exception e) {
                String msg = (_inMem) ? "bad-inmem-method"
                    : "bad-datastore-method";
                throw new UserException(_loc.get(msg, methName, cls));
            }
            if (!Modifier.isStatic(meth.getModifiers()))
                throw new UserException(_loc.get("method-not-static", meth));
            if (!ResultObjectProvider.class.isAssignableFrom(
                meth.getReturnType()))
                throw new UserException(_loc.get("method-return-type-invalid",
                    meth, meth.getReturnType()));
            _meth = meth;
        }

        @Override
        public OrderedMap<Object, Class<?>> getOrderedParameterTypes(StoreQuery q) {
            return ((MethodStoreQuery) q).bindParameterTypes();
		}

        @Override
        public Object[] toParameterArray(StoreQuery q, Map userParams) {
            if (userParams == null || userParams.isEmpty())
                return StoreQuery.EMPTY_OBJECTS;

            OrderedMap<Object, Class<?>> paramTypes = getOrderedParameterTypes(q);
            Object[] arr = new Object[userParams.size()];
            int base = positionalParameterBase(userParams.keySet());
            for (Object key : paramTypes.keySet()) {
                int idx = (key instanceof Integer)
                    ? (Integer) key - base
                    : paramTypes.indexOf(key);
                if (idx >= arr.length || idx < 0)
                        throw new UserException(_loc.get("gap-query-param",
                            new Object[]{q.getContext().getQueryString(), key,
                            userParams.size(), userParams}));
                    arr[idx] = userParams.get(key);
            }
            return arr;
        }

        /**
         * Return the base (generally 0 or 1) to use for positional parameters.
         */
        private static int positionalParameterBase(Collection params) {
            int low = Integer.MAX_VALUE;
            Object obj;
            int val;
            for (Object param : params) {
                obj = param;
                if (!(obj instanceof Number))
                    return 0; // use 0 base when params are mixed types

                val = ((Number) obj).intValue();
                if (val == 0)
                    return val;
                if (val < low)
                    low = val;
            }
            return low;
        }
	}
}
