/*
 * 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.Expr;
import io.ebean.Expression;
import io.ebean.ExpressionList;
import io.ebean.Query;
import org.springframework.data.domain.Sort;
import org.springframework.data.mapping.PropertyPath;
import org.springframework.data.repository.query.ReturnedType;
import org.springframework.data.repository.query.parser.AbstractQueryCreator;
import org.springframework.data.repository.query.parser.Part;
import org.springframework.data.repository.query.parser.PartTree;
import org.springframework.util.Assert;

import java.util.Collection;
import java.util.Iterator;
import java.util.List;

/**
 * EbeanQueryWrapper creator to create a {@link io.ebean.Expression} from a {@link PartTree}.
 *
 * @author Xuegui Yuan
 */
public class EbeanQueryCreator extends AbstractQueryCreator<Query, Expression> {

    private final ExpressionList root;
    private final ParameterMetadataProvider provider;
    private final ReturnedType returnedType;
    private final PartTree tree;

    /**
     * Create a new {@link EbeanQueryCreator}.
     *
     * @param tree           must not be {@literal null}.
     * @param type           must not be {@literal null}.
     * @param expressionList must not be {@literal null}.
     * @param provider       must not be {@literal null}.
     */
    public EbeanQueryCreator(PartTree tree, ReturnedType type, ExpressionList expressionList,
                             ParameterMetadataProvider provider) {
        super(tree);
        this.tree = tree;

        this.root = expressionList;
        this.provider = provider;
        this.returnedType = type;
    }

    /**
     * Returns all {@link ParameterMetadataProvider.ParameterMetadata} created when creating the query.
     *
     * @return the parameterExpressions
     */
    public List<ParameterMetadataProvider.ParameterMetadata<?>> getParameterExpressions() {
        return provider.getExpressions();
    }

    /*
     * (non-Javadoc)
     * @see org.springframework.data.repository.query.parser.AbstractQueryCreator#create(org.springframework.data.repository.query.parser.Part, java.util.Iterator)
     */
    @Override
    protected Expression create(Part part, Iterator<Object> iterator) {
        return toExpression(part, root);
    }

    /*
     * (non-Javadoc)
     * @see org.springframework.data.repository.query.parser.AbstractQueryCreator#and(org.springframework.data.repository.query.parser.Part, java.lang.Object, java.util.Iterator)
     */
    @Override
    protected Expression and(Part part, Expression base, Iterator<Object> iterator) {
        return Expr.and(base, toExpression(part, root));
    }

    /*
     * (non-Javadoc)
     * @see org.springframework.data.repository.query.parser.AbstractQueryCreator#or(java.lang.Object, java.lang.Object)
     */
    @Override
    protected Expression or(Expression base, Expression expression) {
        return Expr.or(base, expression);
    }

    /**
     * Finalizes the given {@link ExpressionList} and applies the given sort.
     */
    @Override
    protected final Query complete(Expression expression, Sort sort) {
        return root.add(expression).query();
    }

    /**
     * Creates a {@link ExpressionList} from the given {@link Part}.
     *
     * @param part
     * @param root
     * @return
     */
    private Expression toExpression(Part part, ExpressionList<?> root) {
        return new ExpressionBuilder(part, root).build();
    }

    /**
     * Simple builder to contain logic to create Ebean {@link Expression}s from {@link Part}s.
     *
     * @author Xuegui Yuan
     */
    @SuppressWarnings({"unchecked", "rawtypes"})
    private class ExpressionBuilder {

        private final Part part;
        private final ExpressionList root;

        /**
         * Creates a new {@link ExpressionBuilder} for the given {@link Part} and {@link ExpressionList}.
         *
         * @param part must not be {@literal null}.
         * @param root must not be {@literal null}.
         */
        public ExpressionBuilder(Part part, ExpressionList<?> root) {
            Assert.notNull(part, "Part must not be null!");
            Assert.notNull(root, "ExpressionList must not be null!");
            this.part = part;
            this.root = root;
        }

        /**
         * Builds a Ebean {@link Expression} from the underlying {@link Part}.
         *
         * @return
         */
        public Expression build() {
            PropertyPath property = part.getProperty();
            Part.Type type = part.getType();

            switch (type) {
                case BETWEEN:
                    ParameterMetadataProvider.ParameterMetadata<Comparable> first = provider.next(part);
                    ParameterMetadataProvider.ParameterMetadata<Comparable> second = provider.next(part);
                    return Expr.between(property.toDotPath(), first.getParameterValue(), second.getParameterValue());
                case AFTER:
                case GREATER_THAN:
                    return Expr.gt(property.toDotPath(), provider.next(part).getParameterValue());
                case GREATER_THAN_EQUAL:
                    return Expr.ge(property.toDotPath(), provider.next(part).getParameterValue());
                case BEFORE:
                case LESS_THAN:
                    return Expr.lt(property.toDotPath(), provider.next(part).getParameterValue());
                case LESS_THAN_EQUAL:
                    return Expr.le(property.toDotPath(), provider.next(part).getParameterValue());
                case IS_NULL:
                    return Expr.isNull(property.toDotPath());
                case IS_NOT_NULL:
                    return Expr.isNotNull(property.toDotPath());
                case NOT_IN:
                    ParameterMetadataProvider.ParameterMetadata<? extends Collection> pmNotIn = provider.next(part, Collection.class);
                    return Expr.not(Expr.in(property.toDotPath(), ParameterMetadataProvider.ParameterMetadata.toCollection(pmNotIn.getParameterValue())));
                case IN:
                    ParameterMetadataProvider.ParameterMetadata<? extends Collection> pmIn = provider.next(part, Collection.class);
                    return Expr.in(property.toDotPath(), ParameterMetadataProvider.ParameterMetadata.toCollection(pmIn.getParameterValue()));
                case STARTING_WITH:
                    return Expr.startsWith(property.toDotPath(), (String) provider.next(part).getParameterValue());
                case ENDING_WITH:
                    return Expr.endsWith(property.toDotPath(), (String) provider.next(part).getParameterValue());
                case CONTAINING:
                    return Expr.contains(property.toDotPath(), (String) provider.next(part).getParameterValue());
                case NOT_CONTAINING:
                    return Expr.not(Expr.contains(property.toDotPath(), (String) provider.next(part).getParameterValue()));
                case LIKE:
                    return Expr.like(property.toDotPath(), (String) provider.next(part).getParameterValue());
                case NOT_LIKE:
                    return Expr.not(Expr.like(property.toDotPath(), (String) provider.next(part).getParameterValue()));
                case TRUE:
                    return Expr.eq(property.toDotPath(), true);
                case FALSE:
                    return Expr.eq(property.toDotPath(), false);
                case SIMPLE_PROPERTY:
                    ParameterMetadataProvider.ParameterMetadata<Object> pmEquals = provider.next(part);
                    return pmEquals.isIsNullParameter() ? Expr.isNull(property.toDotPath())
                            : Expr.eq(property.toDotPath(), pmEquals.getParameterValue());
                case NEGATING_SIMPLE_PROPERTY:
                    ParameterMetadataProvider.ParameterMetadata<Object> pmNot = provider.next(part);
                    return pmNot.isIsNullParameter() ? Expr.isNull(property.toDotPath())
                            : Expr.ne(property.toDotPath(), pmNot.getParameterValue());
//        case IS_EMPTY:
//          return Expr.isEmpty(property.toDotPath());
//        case IS_NOT_EMPTY:
//          return Expr.isNotEmpty(property.toDotPath());
                default:
                    throw new IllegalArgumentException("Unsupported keyword " + type);
            }
        }
    }
}