/*
 * 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.dao.InvalidDataAccessApiUsageException;
import org.springframework.data.domain.Slice;
import org.springframework.data.repository.core.support.SurroundingTransactionDetectorMethodInterceptor;
import org.springframework.data.repository.query.ParameterAccessor;
import org.springframework.data.repository.query.Parameters;
import org.springframework.data.repository.query.ParametersParameterAccessor;
import org.springframework.util.Assert;

/**
 * Set of classes to contain query execution strategies. Depending (mostly) on the return type of a
 * {@link org.springframework.data.repository.query.QueryMethod} a {@link AbstractStringBasedEbeanQuery} can be executed
 * in various flavors.
 *
 * @author Xuegui Yuan
 */
public abstract class AbstractEbeanQueryExecution {

    /**
     * Executes the given {@link AbstractStringBasedEbeanQuery} with the given {@link ParameterBinder}.
     *
     * @param query  must not be {@literal null}.
     * @param values must not be {@literal null}.
     * @return
     */
    public Object execute(AbstractEbeanQuery query, Object[] values) {
        Assert.notNull(query, "AbstractEbeanQuery must not be null!");
        Assert.notNull(values, "Values must not be null!");

        return doExecute(query, values);
    }

    /**
     * Method to implement {@link AbstractStringBasedEbeanQuery} executions by single enum values.
     *
     * @param query
     * @param values
     * @return
     */
    protected abstract Object doExecute(AbstractEbeanQuery query, Object[] values);

    /**
     * Executes the query to return a simple collection of entities.
     */
    static class CollectionExecution extends AbstractEbeanQueryExecution {

        @Override
        protected Object doExecute(AbstractEbeanQuery repositoryQuery, Object[] values) {
            EbeanQueryWrapper createQuery = repositoryQuery.createQuery(values);
            return createQuery.findList();
        }
    }

    /**
     * Executes the query to return a {@link Slice} of entities.
     *
     * @author Xuegui Yuan
     */
    static class SlicedExecution extends AbstractEbeanQueryExecution {

        private final Parameters<?, ?> parameters;

        /**
         * Creates a new {@link SlicedExecution} using the given {@link Parameters}.
         *
         * @param parameters must not be {@literal null}.
         */
        public SlicedExecution(Parameters<?, ?> parameters) {
            this.parameters = parameters;
        }

        /*
         * (non-Javadoc)
         * @see org.springframework.data.ebean.repository.query.AbstractEbeanQueryExecution#doExecute(org.springframework.data.ebean.repository.query.AbstractEbeanQuery, java.lang.Object[])
         */
        @Override
        @SuppressWarnings("unchecked")
        protected Object doExecute(AbstractEbeanQuery query, Object[] values) {
            ParametersParameterAccessor accessor = new ParametersParameterAccessor(parameters, values);
            EbeanQueryWrapper createQuery = query.createQuery(values);
            return createQuery.findSlice(accessor.getPageable());
        }
    }

    /**
     * Executes the {@link AbstractStringBasedEbeanQuery} to return a {@link org.springframework.data.domain.Page} of
     * entities.
     */
    static class PagedExecution extends AbstractEbeanQueryExecution {

        private final Parameters<?, ?> parameters;

        public PagedExecution(Parameters<?, ?> parameters) {

            this.parameters = parameters;
        }

        @Override
        @SuppressWarnings("unchecked")
        protected Object doExecute(final AbstractEbeanQuery repositoryQuery, final Object[] values) {
            ParameterAccessor accessor = new ParametersParameterAccessor(parameters, values);
            EbeanQueryWrapper createQuery = repositoryQuery.createQuery(values);
            return createQuery.findPage(accessor.getPageable());
        }
    }


    /**
     * Executes a {@link AbstractStringBasedEbeanQuery} to return a single entity.
     */
    static class SingleEntityExecution extends AbstractEbeanQueryExecution {

        @Override
        protected Object doExecute(AbstractEbeanQuery query, Object[] values) {
            EbeanQueryWrapper createQuery = query.createQuery(values);
            return createQuery.findOne();
        }
    }

    /**
     * Executes a update query such as an update, insert or delete.
     */
    static class UpdateExecution extends AbstractEbeanQueryExecution {

        private final EbeanServer ebeanServer;

        /**
         * Creates an execution that automatically clears the given {@link EbeanServer} after execution if the given
         * {@link EbeanServer} is not {@literal null}.
         *
         * @param ebeanServer
         */
        public UpdateExecution(EbeanQueryMethod method, EbeanServer ebeanServer) {

            Class<?> returnType = method.getReturnType();

            boolean isVoid = void.class.equals(returnType) || Void.class.equals(returnType);
            boolean isInt = int.class.equals(returnType) || Integer.class.equals(returnType);

            Assert.isTrue(isInt || isVoid, "Modifying queries can only use void or int/Integer as return type!");

            this.ebeanServer = ebeanServer;
        }

        @Override
        protected Object doExecute(AbstractEbeanQuery query, Object[] values) {
            EbeanQueryWrapper createQuery = query.createQuery(values);
            return createQuery.update();
        }
    }

    /**
     * {@link AbstractEbeanQueryExecution} removing entities matching the query.
     *
     * @author Xuegui Yuan
     */
    static class DeleteExecution extends AbstractEbeanQueryExecution {

        private final EbeanServer ebeanServer;

        public DeleteExecution(EbeanServer ebeanServer) {
            this.ebeanServer = ebeanServer;
        }

        /*
         * (non-Javadoc)
         * @see org.springframework.data.ebean.repository.query.AbstractEbeanQueryExecution#doExecute(org.springframework.data.ebean.repository.query.AbstractEbeanQuery, java.lang.Object[])
         */
        @Override
        protected Object doExecute(AbstractEbeanQuery ebeanQuery, Object[] values) {
            EbeanQueryWrapper createQuery = ebeanQuery.createQuery(values);
            return createQuery.delete();
        }
    }

    /**
     * {@link AbstractEbeanQueryExecution} performing an exists check on the query.
     *
     * @author Xuegui Yuan
     */
    static class ExistsExecution extends AbstractEbeanQueryExecution {

        @Override
        protected Object doExecute(AbstractEbeanQuery ebeanQuery, Object[] values) {
            EbeanQueryWrapper createQuery = ebeanQuery.createQuery(values);
            return createQuery.isExists();
        }
    }

    /**
     * {@link AbstractEbeanQueryExecution} executing a Java 8 Stream.
     *
     * @author Xuegui Yuan
     */
    static class StreamExecution extends AbstractEbeanQueryExecution {

        private static final String NO_SURROUNDING_TRANSACTION = "You're trying to execute a streaming query method without a surrounding transaction that keeps the connection open so that the Stream can actually be consumed. Make sure the code consuming the stream uses @Transactional or any other way of declaring a (read-only) transaction.";

        /*
         * (non-Javadoc)
         * @see org.springframework.data.ebean.repository.query.AbstractEbeanQueryExecution#doExecute(org.springframework.data.ebean.repository.query.AbstractEbeanQuery, java.lang.Object[])
         */
        @Override
        protected Object doExecute(final AbstractEbeanQuery ebeanQuery, Object[] values) {
            if (!SurroundingTransactionDetectorMethodInterceptor.INSTANCE.isSurroundingTransactionActive()) {
                throw new InvalidDataAccessApiUsageException(NO_SURROUNDING_TRANSACTION);
            }

            EbeanQueryWrapper createQuery = ebeanQuery.createQuery(values);
            return createQuery.findStream();
        }
    }
}