/*
 * 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.ignite.springdata20.repository.query;

import java.lang.reflect.Method;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.repository.core.RepositoryMetadata;
import org.springframework.data.repository.query.parser.Part;
import org.springframework.data.repository.query.parser.PartTree;

/**
 * Ignite query generator for Spring Data framework.
 */
public class IgniteQueryGenerator {
    /** */
    private IgniteQueryGenerator() {
        // No-op.
    }

    /**
     * @param mtd      Method.
     * @param metadata Metadata.
     * @return Generated ignite query.
     */
    public static IgniteQuery generateSql(Method mtd, RepositoryMetadata metadata) {
        PartTree parts = new PartTree(mtd.getName(), metadata.getDomainType());

        boolean isCountOrFieldQuery = parts.isCountProjection();

        StringBuilder sql = new StringBuilder();

        if (parts.isDelete()) {
            sql.append("DELETE ");

            // For the DML queries aside from SELECT *, they should run over SqlFieldQuery
            isCountOrFieldQuery = true;
        }
        else {
            sql.append("SELECT ");

            if (parts.isDistinct())
                throw new UnsupportedOperationException("DISTINCT clause in not supported.");

            if (isCountOrFieldQuery)
                sql.append("COUNT(1) ");
            else
                sql.append("* ");
        }

        sql.append("FROM ").append(metadata.getDomainType().getSimpleName());

        if (parts.iterator().hasNext()) {
            sql.append(" WHERE ");

            for (PartTree.OrPart orPart : parts) {
                sql.append("(");

                for (Part part : orPart) {
                    handleQueryPart(sql, part);
                    sql.append(" AND ");
                }

                sql.delete(sql.length() - 5, sql.length());

                sql.append(") OR ");
            }

            sql.delete(sql.length() - 4, sql.length());
        }

        addSorting(sql, parts.getSort());

        if (parts.isLimiting()) {
            sql.append(" LIMIT ");
            sql.append(parts.getMaxResults().intValue());
        }

        return new IgniteQuery(sql.toString(), isCountOrFieldQuery, false, true, getOptions(mtd));
    }

    /**
     * Add a dynamic part of query for the sorting support.
     *
     * @param sql  SQL text string.
     * @param sort Sort method.
     * @return Sorting criteria in StringBuilder.
     */
    public static StringBuilder addSorting(StringBuilder sql, Sort sort) {
        if (sort != null && sort != Sort.unsorted()) {
            sql.append(" ORDER BY ");

            for (Sort.Order order : sort) {
                sql.append(order.getProperty()).append(" ").append(order.getDirection());

                if (order.getNullHandling() != Sort.NullHandling.NATIVE) {
                    sql.append(" ").append("NULL ");

                    switch (order.getNullHandling()) {
                        case NULLS_FIRST:
                            sql.append("FIRST");
                            break;
                        case NULLS_LAST:
                            sql.append("LAST");
                            break;
                        default:
                    }
                }
                sql.append(", ");
            }

            sql.delete(sql.length() - 2, sql.length());
        }

        return sql;
    }

    /**
     * Add a dynamic part of a query for the pagination support.
     *
     * @param sql      Builder instance.
     * @param pageable Pageable instance.
     * @return Builder instance.
     */
    public static StringBuilder addPaging(StringBuilder sql, Pageable pageable) {

        addSorting(sql, pageable.getSort());

        sql.append(" LIMIT ").append(pageable.getPageSize()).append(" OFFSET ").append(pageable.getOffset());

        return sql;
    }

    /**
     * Determines whether query is dynamic or not (by list of method parameters)
     *
     * @param mtd Method.
     * @return type of options
     */
    public static IgniteQuery.Option getOptions(Method mtd) {
        IgniteQuery.Option option = IgniteQuery.Option.NONE;

        Class<?>[] types = mtd.getParameterTypes();
        if (types.length > 0) {
            Class<?> type = types[types.length - 1];

            if (Sort.class.isAssignableFrom(type))
                option = IgniteQuery.Option.SORTING;
            else if (Pageable.class.isAssignableFrom(type))
                option = IgniteQuery.Option.PAGINATION;
        }

        for (int i = 0; i < types.length - 1; i++) {
            Class<?> tp = types[i];

            if (tp == Sort.class || tp == Pageable.class)
                throw new AssertionError("Sort and Pageable parameters are allowed only in the last position");
        }

        return option;
    }

    /**
     * Transform part to qryStr expression
     */
    private static void handleQueryPart(StringBuilder sql, Part part) {
        sql.append("(");

        sql.append(part.getProperty());

        switch (part.getType()) {
            case SIMPLE_PROPERTY:
                sql.append("=?");
                break;
            case NEGATING_SIMPLE_PROPERTY:
                sql.append("<>?");
                break;
            case GREATER_THAN:
                sql.append(">?");
                break;
            case GREATER_THAN_EQUAL:
                sql.append(">=?");
                break;
            case LESS_THAN:
                sql.append("<?");
                break;
            case LESS_THAN_EQUAL:
                sql.append("<=?");
                break;
            case IS_NOT_NULL:
                sql.append(" IS NOT NULL");
                break;
            case IS_NULL:
                sql.append(" IS NULL");
                break;
            case BETWEEN:
                sql.append(" BETWEEN ? AND ?");
                break;
            case FALSE:
                sql.append(" = FALSE");
                break;
            case TRUE:
                sql.append(" = TRUE");
                break;
            //TODO: review this legacy code, LIKE should be -> LIKE ?
            case LIKE:
            case CONTAINING:
                sql.append(" LIKE '%' || ? || '%'");
                break;
            case NOT_CONTAINING:
                //TODO: review this legacy code, NOT_LIKE should be -> NOT LIKE ?
            case NOT_LIKE:
                sql.append(" NOT LIKE '%' || ? || '%'");
                break;
            case STARTING_WITH:
                sql.append(" LIKE  ? || '%'");
                break;
            case ENDING_WITH:
                sql.append(" LIKE '%' || ?");
                break;
            case IN:
                sql.append(" IN ?");
                break;
            case NOT_IN:
                sql.append(" NOT IN ?");
                break;
            case REGEX:
                sql.append(" REGEXP ?");
                break;
            case NEAR:
            case AFTER:
            case BEFORE:
            case EXISTS:
            default:
                throw new UnsupportedOperationException(part.getType() + " is not supported!");
        }

        sql.append(")");
    }
}
