/*
 * Copyright 2017 the original author or authors.
 *
 * Licensed 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.springframework.data.ebean.repository.query;

import io.ebean.EbeanServer;
import org.springframework.data.ebean.annotation.Query;
import org.springframework.data.projection.ProjectionFactory;
import org.springframework.data.repository.core.NamedQueries;
import org.springframework.data.repository.core.RepositoryMetadata;
import org.springframework.data.repository.query.QueryLookupStrategy;
import org.springframework.data.repository.query.QueryLookupStrategy.Key;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.data.repository.query.RepositoryQuery;
import org.springframework.util.Assert;

import java.lang.reflect.Method;

/**
 * EbeanQueryWrapper lookup strategy to execute finders.
 *
 * @author Xuegui Yuan
 */
public final class EbeanQueryLookupStrategy {

    /**
     * Private constructor to prevent instantiation.
     */
    private EbeanQueryLookupStrategy() {
    }

    /**
     * Creates a {@link QueryLookupStrategy} for the given {@link EbeanServer} and {@link Key}.
     *
     * @param ebeanServer               must not be {@literal null}.
     * @param key                       may be {@literal null}.
     * @param evaluationContextProvider must not be {@literal null}.
     * @return
     */
    public static QueryLookupStrategy create(EbeanServer ebeanServer, Key key,
                                             QueryMethodEvaluationContextProvider evaluationContextProvider) {

        Assert.notNull(ebeanServer, "EbeanServer must not be null!");
        Assert.notNull(evaluationContextProvider, "EvaluationContextProvider must not be null!");

        switch (key != null ? key : Key.CREATE_IF_NOT_FOUND) {
            case CREATE:
                return new CreateQueryLookupStrategy(ebeanServer);
            case USE_DECLARED_QUERY:
                return new DeclaredQueryLookupStrategy(ebeanServer, evaluationContextProvider);
            case CREATE_IF_NOT_FOUND:
                return new CreateIfNotFoundQueryLookupStrategy(ebeanServer, new CreateQueryLookupStrategy(ebeanServer),
                        new DeclaredQueryLookupStrategy(ebeanServer, evaluationContextProvider));
            default:
                throw new IllegalArgumentException(String.format("Unsupported query lookup strategy %s!", key));
        }
    }

    /**
     * {@link QueryLookupStrategy} to create a query from the method name.
     *
     * @author Xuegui Yuan
     */
    private static class CreateQueryLookupStrategy extends AbstractQueryLookupStrategy {

        public CreateQueryLookupStrategy(EbeanServer ebeanServer) {

            super(ebeanServer);
        }

        @Override
        protected RepositoryQuery resolveQuery(EbeanQueryMethod method, EbeanServer ebeanServer, NamedQueries namedQueries) {

            try {
                return new PartTreeEbeanQuery(method, ebeanServer);
            } catch (IllegalArgumentException e) {
                throw new IllegalArgumentException(
                        String.format("Could not create query metamodel for method %s!", method.toString()), e);
            }
        }

    }

    /**
     * Base class for {@link QueryLookupStrategy} implementations that need access to an {@link EbeanServer}.
     *
     * @author Oliver Gierke
     * @author Thomas Darimont
     */
    private abstract static class AbstractQueryLookupStrategy implements QueryLookupStrategy {

        private final EbeanServer ebeanServer;

        /**
         * Creates a new {@link AbstractQueryLookupStrategy}.
         *
         * @param ebeanServer
         */
        public AbstractQueryLookupStrategy(EbeanServer ebeanServer) {
            this.ebeanServer = ebeanServer;
        }

        @Override
        public final RepositoryQuery resolveQuery(Method method, RepositoryMetadata metadata, ProjectionFactory factory,
                                                  NamedQueries namedQueries) {
            return resolveQuery(new EbeanQueryMethod(method, metadata, factory), ebeanServer, namedQueries);
        }

        /**
         * Resolve query to return .RepositoryQuery
         *
         * @param method
         * @param ebeanServer
         * @param namedQueries
         * @return RepositoryQuery
         */
        protected abstract RepositoryQuery resolveQuery(EbeanQueryMethod method, EbeanServer ebeanServer, NamedQueries namedQueries);
    }

    /**
     * {@link QueryLookupStrategy} to try to detect a declared query first (
     * {@link Query}, ebean named query). In case none is found we fall back on
     * query creation.
     *
     * @author Oliver Gierke
     * @author Thomas Darimont
     */
    private static class CreateIfNotFoundQueryLookupStrategy extends AbstractQueryLookupStrategy {

        private final DeclaredQueryLookupStrategy lookupStrategy;
        private final CreateQueryLookupStrategy createStrategy;

        /**
         * Creates a new {@link CreateIfNotFoundQueryLookupStrategy}.
         *
         * @param ebeanServer
         * @param createStrategy
         * @param lookupStrategy
         */
        public CreateIfNotFoundQueryLookupStrategy(EbeanServer ebeanServer,
                                                   CreateQueryLookupStrategy createStrategy, DeclaredQueryLookupStrategy lookupStrategy) {
            super(ebeanServer);

            this.createStrategy = createStrategy;
            this.lookupStrategy = lookupStrategy;
        }

        /*
         * (non-Javadoc)
         * @see org.springframework.data.ebean.repository.query.ebeanQueryLookupStrategy.AbstractQueryLookupStrategy#resolveQuery(org.springframework.data.ebean.repository.query.ebeanQueryMethod, javax.persistence.EntityManager, org.springframework.data.repository.core.NamedQueries)
         */
        @Override
        protected RepositoryQuery resolveQuery(EbeanQueryMethod method, EbeanServer ebeanServer, NamedQueries namedQueries) {
            try {
                return lookupStrategy.resolveQuery(method, ebeanServer, namedQueries);
            } catch (IllegalStateException e) {
                return createStrategy.resolveQuery(method, ebeanServer, namedQueries);
            }
        }
    }

    /**
     * {@link QueryLookupStrategy} that tries to detect a declared query declared via {@link io.ebean.Query} annotation followed by
     * a ebean named query lookup.
     *
     * @author Oliver Gierke
     * @author Thomas Darimont
     */
    private static class DeclaredQueryLookupStrategy extends AbstractQueryLookupStrategy {

        private final QueryMethodEvaluationContextProvider evaluationContextProvider;

        /**
         * Creates a new {@link DeclaredQueryLookupStrategy}.
         *
         * @param ebeanServer
         * @param evaluationContextProvider
         */
        public DeclaredQueryLookupStrategy(EbeanServer ebeanServer,
                                           QueryMethodEvaluationContextProvider evaluationContextProvider) {
            super(ebeanServer);
            this.evaluationContextProvider = evaluationContextProvider;
        }

        /*
         * (non-Javadoc)
         * @see org.springframework.data.ebean.repository.query.ebeanQueryLookupStrategy.AbstractQueryLookupStrategy#resolveQuery(org.springframework.data.ebean.repository.query.ebeanQueryMethod, javax.persistence.EntityManager, org.springframework.data.repository.core.NamedQueries)
         */
        @Override
        protected RepositoryQuery resolveQuery(EbeanQueryMethod method, EbeanServer ebeanServer, NamedQueries namedQueries) {
            RepositoryQuery query = EbeanQueryFactory.INSTANCE.fromQueryAnnotation(method, ebeanServer, evaluationContextProvider);

            if (null != query) {
                return query;
            }

            String name = method.getNamedQueryName();
            if (namedQueries.hasQuery(name)) {
                return EbeanQueryFactory.INSTANCE.fromMethodWithQueryString(method, ebeanServer, namedQueries.getQuery(name),
                        evaluationContextProvider);
            }

            query = NamedEbeanQuery.lookupFrom(method, ebeanServer);

            if (null != query) {
                return query;
            }

            throw new IllegalStateException(
                    String.format("Did neither find a NamedQuery nor an annotated query for method %s!", method));
        }
    }
}