/*
 * 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.iceberg.util;

import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Deque;
import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.function.Function;
import org.apache.iceberg.relocated.com.google.common.base.Preconditions;
import org.apache.iceberg.relocated.com.google.common.collect.ImmutableList;
import org.apache.iceberg.relocated.com.google.common.collect.Iterables;
import org.apache.iceberg.relocated.com.google.common.collect.Lists;

public class BinPacking {
  public static class ListPacker<T> {
    private final long targetWeight;
    private final int lookback;
    private final boolean largestBinFirst;
    private final long maxItemsPerBin;

    public ListPacker(long targetWeight, int lookback, boolean largestBinFirst) {
      this(targetWeight, lookback, largestBinFirst, Long.MAX_VALUE);
    }

    public ListPacker(
        long targetWeight, int lookback, boolean largestBinFirst, long maxItemsPerBin) {
      this.targetWeight = targetWeight;
      this.lookback = lookback;
      this.largestBinFirst = largestBinFirst;
      this.maxItemsPerBin = maxItemsPerBin;
    }

    public List<List<T>> packEnd(List<T> items, Function<T, Long> weightFunc) {
      return Lists.reverse(
          ImmutableList.copyOf(
              Iterables.transform(
                  new PackingIterable<>(
                      Lists.reverse(items),
                      targetWeight,
                      lookback,
                      weightFunc,
                      largestBinFirst,
                      maxItemsPerBin),
                  Lists::reverse)));
    }

    public List<List<T>> pack(Iterable<T> items, Function<T, Long> weightFunc) {
      return ImmutableList.copyOf(
          new PackingIterable<>(
              items, targetWeight, lookback, weightFunc, largestBinFirst, maxItemsPerBin));
    }
  }

  public static class PackingIterable<T> implements Iterable<List<T>> {
    private final Iterable<T> iterable;
    private final long targetWeight;
    private final int lookback;
    private final long maxSize;
    private final Function<T, Long> weightFunc;
    private final boolean largestBinFirst;

    public PackingIterable(
        Iterable<T> iterable, long targetWeight, int lookback, Function<T, Long> weightFunc) {
      this(iterable, targetWeight, lookback, weightFunc, false, Long.MAX_VALUE);
    }

    public PackingIterable(
        Iterable<T> iterable,
        long targetWeight,
        int lookback,
        Function<T, Long> weightFunc,
        boolean largestBinFirst) {
      this(iterable, targetWeight, lookback, weightFunc, largestBinFirst, Long.MAX_VALUE);
    }

    public PackingIterable(
        Iterable<T> iterable,
        long targetWeight,
        int lookback,
        Function<T, Long> weightFunc,
        boolean largestBinFirst,
        long maxSize) {
      Preconditions.checkArgument(
          lookback > 0, "Bin look-back size must be greater than 0: %s", lookback);
      this.iterable = iterable;
      this.targetWeight = targetWeight;
      this.lookback = lookback;
      this.maxSize = maxSize;
      this.weightFunc = weightFunc;
      this.largestBinFirst = largestBinFirst;
    }

    @Override
    public Iterator<List<T>> iterator() {
      return new PackingIterator<>(
          iterable.iterator(), targetWeight, lookback, maxSize, weightFunc, largestBinFirst);
    }
  }

  private static class PackingIterator<T> implements Iterator<List<T>> {
    private final Deque<Bin<T>> bins = Lists.newLinkedList();
    private final Iterator<T> items;
    private final long targetWeight;
    private final int lookback;
    private final long maxSize;
    private final Function<T, Long> weightFunc;
    private final boolean largestBinFirst;

    private PackingIterator(
        Iterator<T> items,
        long targetWeight,
        int lookback,
        long maxSize,
        Function<T, Long> weightFunc,
        boolean largestBinFirst) {
      this.items = items;
      this.targetWeight = targetWeight;
      this.lookback = lookback;
      this.maxSize = maxSize;
      this.weightFunc = weightFunc;
      this.largestBinFirst = largestBinFirst;
    }

    @Override
    public boolean hasNext() {
      return items.hasNext() || !bins.isEmpty();
    }

    @Override
    public List<T> next() {
      while (items.hasNext()) {
        T item = items.next();

        long weight = weightFunc.apply(item);
        Bin<T> bin = findBin(weight);

        if (bin != null) {
          bin.add(item, weight);

        } else {
          bin = newBin();
          bin.add(item, weight);
          bins.addLast(bin);

          if (bins.size() > lookback) {
            Bin<T> binToRemove;
            if (largestBinFirst) {
              binToRemove = removeLargestBin(bins);
            } else {
              binToRemove = bins.removeFirst();
            }
            return ImmutableList.copyOf(binToRemove.items());
          }
        }
      }

      if (bins.isEmpty()) {
        throw new NoSuchElementException();
      }

      return ImmutableList.copyOf(bins.removeFirst().items());
    }

    private Bin<T> findBin(long weight) {
      for (Bin<T> bin : bins) {
        if (bin.canAdd(weight)) {
          return bin;
        }
      }
      return null;
    }

    private Bin<T> newBin() {
      return new Bin<>(targetWeight, maxSize);
    }

    private static <T> Bin<T> removeLargestBin(Collection<Bin<T>> bins) {
      // Iterate through all bins looking for one with maximum weight, taking O(n) time.
      Bin<T> maxBin = Collections.max(bins, Comparator.comparingLong(Bin::weight));

      // Sanity check: we have removed maxBin from list of bins.
      if (bins.remove(maxBin)) {
        return maxBin;
      } else {
        throw new NoSuchElementException();
      }
    }
  }

  private static class Bin<T> {
    private final long targetWeight;
    private final long maxSize;
    private final List<T> items = Lists.newArrayList();
    private long binWeight = 0L;
    private int binSize = 0;

    Bin(long targetWeight, long maxSize) {
      this.targetWeight = targetWeight;
      this.maxSize = maxSize;
    }

    List<T> items() {
      return items;
    }

    boolean canAdd(long weight) {
      return binWeight + weight <= targetWeight && binSize < maxSize;
    }

    void add(T item, long weight) {
      this.binWeight += weight;
      this.binSize++;
      items.add(item);
    }

    long weight() {
      return binWeight;
    }
  }
}
