/*
 * Copyright 2010-2013 Ning, Inc.
 * Copyright 2014-2018 Groupon, Inc
 * Copyright 2014-2018 The Billing Project, LLC
 *
 * The Billing Project 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.killbill.billing.util.entity;

import java.io.Closeable;
import java.io.IOException;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;

import javax.annotation.Nullable;

import com.google.common.collect.ImmutableList;

// Assumes the original offset starts at zero.
public class DefaultPagination<T> implements Pagination<T>, Closeable {

    private final Long currentOffset;
    private final Long limit;
    private final Long totalNbRecords;
    private final Long maxNbRecords;
    private final Iterator<T> delegateIterator;

    // Builders when the streaming API can't be used (should only be used for tests)
    // Notes: elements should be the entire records set (regardless of filtering) otherwise maxNbRecords won't be accurate
    public static <T> Pagination<T> build(final Long offset, final Long limit, final Collection<T> elements) {
        return build(offset, limit, elements.size(), elements);
    }

    public static <T> Pagination<T> build(final Long offset, final Long limit, final Integer maxNbRecords, final Collection<T> elements) {
        final List<T> allResults = ImmutableList.<T>copyOf(elements);

        final List<T> results;
        if (offset >= allResults.size()) {
            results = ImmutableList.<T>of();
        } else if (offset + limit > allResults.size()) {
            results = allResults.subList(offset.intValue(), allResults.size());
        } else {
            results = allResults.subList(offset.intValue(), offset.intValue() + limit.intValue());
        }
        return new DefaultPagination<T>(offset, limit, (long) results.size(), (long) maxNbRecords, results.iterator());
    }

    // Constructor for DAO -> API bridge
    public DefaultPagination(final Pagination original, final Long limit, final Iterator<T> delegate) {
        this(original.getCurrentOffset(), limit, original.getTotalNbRecords(), original.getMaxNbRecords(), delegate);
    }

    // Constructor for DAO getAll calls
    public DefaultPagination(final Long maxNbRecords, final Iterator<T> results) {
        this(0L, Long.MAX_VALUE, maxNbRecords, maxNbRecords, results);
    }

    public DefaultPagination(final Long currentOffset, final Long limit,
                             @Nullable final Long totalNbRecords, @Nullable final Long maxNbRecords,
                             final Iterator<T> delegateIterator) {
        this.currentOffset = currentOffset;
        // See DefaultPaginationSqlDaoHelper
        this.limit = Math.abs(limit);
        this.totalNbRecords = totalNbRecords;
        this.maxNbRecords = maxNbRecords;
        this.delegateIterator = delegateIterator;
    }

    @Override
    public void close() throws IOException {
        if (delegateIterator instanceof Closeable) {
            // Always the case with the current implementation (delegateIterator is a org.skife.jdbi.v2.ResultIterator)
            ((Closeable) delegateIterator).close();
        } else {
            while (delegateIterator.hasNext()) {
                delegateIterator.next();
            }
        }
    }

    @Override
    public Iterator<T> iterator() {
        return delegateIterator;
    }

    @Override
    public Long getCurrentOffset() {
        return currentOffset;
    }

    @Override
    public Long getNextOffset() {
        final long candidate = currentOffset + limit;
        if (totalNbRecords != null && candidate >= totalNbRecords) {
            // No more results
            return null;
        } else {
            // When we don't know the total number of records, the next offset
            // returned here won't make sense once the last result is returned.
            // It is the responsibility of the client to handle the pagination stop condition
            // in that case (i.e. check if there is no more results).
            return candidate;
        }
    }

    @Override
    public Long getMaxNbRecords() {
        return maxNbRecords;
    }

    @Override
    public Long getTotalNbRecords() {
        return totalNbRecords;
    }

    @Override
    public String toString() {
        final StringBuilder sb = new StringBuilder("DefaultPagination{");
        sb.append("currentOffset=").append(currentOffset);
        sb.append(", nextOffset=").append(getNextOffset());
        sb.append(", totalNbRecords=").append(totalNbRecords);
        sb.append(", maxNbRecords=").append(maxNbRecords);
        sb.append('}');
        return sb.toString();
    }

    // Expensive! Will compare the content of the iterator
    @Override
    public boolean equals(final Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }

        final DefaultPagination that = (DefaultPagination) o;

        if (totalNbRecords != null ? !totalNbRecords.equals(that.totalNbRecords) : that.totalNbRecords != null) {
            return false;
        }
        if (maxNbRecords != null ? !maxNbRecords.equals(that.maxNbRecords) : that.maxNbRecords != null) {
            return false;
        }
        if (currentOffset != null ? !currentOffset.equals(that.currentOffset) : that.currentOffset != null) {
            return false;
        }
        if (delegateIterator != null ? !ImmutableList.<T>copyOf(delegateIterator).equals(ImmutableList.<T>copyOf(that.delegateIterator)) : that.delegateIterator != null) {
            return false;
        }

        return true;
    }

    @Override
    public int hashCode() {
        int result = currentOffset != null ? currentOffset.hashCode() : 0;
        result = 31 * result + (totalNbRecords != null ? totalNbRecords.hashCode() : 0);
        result = 31 * result + (maxNbRecords != null ? maxNbRecords.hashCode() : 0);
        result = 31 * result + (delegateIterator != null ? delegateIterator.hashCode() : 0);
        return result;
    }
}
