/*
 * Copyright 2017-2020 original 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
 *
 * https://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 io.micronaut.data.runtime.http;

import io.micronaut.context.annotation.Requires;
import io.micronaut.core.convert.ArgumentConversionContext;
import io.micronaut.core.type.Argument;
import io.micronaut.data.model.Pageable;
import io.micronaut.data.model.Sort;
import io.micronaut.data.runtime.config.DataConfiguration;
import io.micronaut.http.HttpParameters;
import io.micronaut.http.HttpRequest;
import io.micronaut.http.bind.binders.RequestArgumentBinder;
import io.micronaut.http.bind.binders.TypedRequestArgumentBinder;

import javax.inject.Singleton;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Collectors;

/**
 * A request argument binder for binding a {@link Pageable} object from the request.
 *
 * @author graemerocher
 * @since 1.0.0
 */
@Requires(classes = RequestArgumentBinder.class)
@Singleton
public class PageableRequestArgumentBinder implements TypedRequestArgumentBinder<Pageable> {

    public static final Argument<Pageable> TYPE = Argument.of(Pageable.class);

    private final DataConfiguration.PageableConfiguration configuration;
    private final Function<String, Sort.Order> sortMapper;

    /**
     * Default constructor.
     * @param configuration The configuration
     */
    protected PageableRequestArgumentBinder(DataConfiguration.PageableConfiguration configuration) {
        this.configuration = configuration;
        sortMapper = s -> {
            String[] tokens = configuration.getSortDelimiterPattern().split(s);
            if (tokens.length == 1) {
                return new Sort.Order(tokens[0], Sort.Order.Direction.ASC, configuration.isSortIgnoreCase());
            } else {
                try {
                    Sort.Order.Direction direction = Sort.Order.Direction.valueOf(tokens[1].toUpperCase(Locale.ENGLISH));
                    return new Sort.Order(tokens[0], direction, configuration.isSortIgnoreCase());
                } catch (IllegalArgumentException e) {
                    return new Sort.Order(tokens[0], Sort.Order.Direction.ASC, configuration.isSortIgnoreCase());
                }
            }
        };
    }

    @Override
    public Argument<Pageable> argumentType() {
        return TYPE;
    }

    @Override
    public BindingResult<Pageable> bind(ArgumentConversionContext<Pageable> context, HttpRequest<?> source) {
        HttpParameters parameters = source.getParameters();
        int page = Math.max(parameters.getFirst(configuration.getPageParameterName(), Integer.class)
                        .orElse(0), 0);
        final int configuredMaxSize = configuration.getMaxPageSize();
        final int defaultSize = configuration.getDefaultPageSize();
        int size = Math.min(parameters.getFirst(configuration.getSizeParameterName(), Integer.class)
                       .orElse(defaultSize), configuredMaxSize);
        String sortParameterName = configuration.getSortParameterName();
        boolean hasSort = parameters.contains(sortParameterName);
        Pageable pageable;
        Sort sort = null;
        if (hasSort) {
            List<String> sortParams = parameters.getAll(sortParameterName);

            List<Sort.Order> orders = sortParams.stream()
                    .map(sortMapper)
                    .collect(Collectors.toList());
            sort = Sort.of(orders);
        }

        if (size < 1) {
            if (page == 0 && configuredMaxSize < 1 && sort == null) {
                pageable = Pageable.UNPAGED;
            } else {
                pageable = Pageable.from(page, defaultSize, sort);
            }
        } else {
            pageable = Pageable.from(page, size, sort);
        }

        return () -> Optional.of(pageable);
    }
}