<?php
/**
 * Box packing (3D bin packing, knapsack problem).
 *
 * @author Doug Wright
 */
namespace D5WEXT\Packing\DVDoug\BoxPacker;

use ArrayIterator;
use IteratorAggregate;
use Traversable;
use function usort;

/**
 * List of boxes available to put items into, ordered by volume.
 *
 * @author Doug Wright
 */
class BoxList implements IteratorAggregate
{
    /**
     * List containing boxes.
     *
     * @var Box[]
     */
    private $list = [];

    /**
     * Has this list already been sorted?
     *
     * @var bool
     */
    private $isSorted = false;

    /**
     * Do a bulk create.
     *
     * @param Box[] $boxes
     *
     * @param bool $preSorted
     * @return BoxList
     */
    public static function fromArray(array $boxes, $preSorted = false)
    {
        $list = new static();
        $list->list = $boxes;
        $list->isSorted = $preSorted;

        return $list;
    }

    /**
     * @return Traversable|Box[]
     */
    #[\ReturnTypeWillChange]
    public function getIterator()
    {
        if (!$this->isSorted) {
            usort($this->list, [$this, 'compare']);
            $this->isSorted = true;
        }

        return new ArrayIterator($this->list);
    }

    /**
     * @param Box $item
     * @return void
     */
    public function insert(Box $item)
    {
        $this->list[] = $item;
    }

    /**
     * @param Box $boxA
     * @param Box $boxB
     * @return int
     */
    public static function compare($boxA, $boxB)
    {
        $boxAVolume = $boxA->getInnerWidth() * $boxA->getInnerLength() * $boxA->getInnerDepth();
        $boxBVolume = $boxB->getInnerWidth() * $boxB->getInnerLength() * $boxB->getInnerDepth();

        $volumeDecider = mixCmp($boxAVolume,$boxBVolume); // try smallest box first

        if ($volumeDecider !== 0) {
            return $volumeDecider;
        }

        $emptyWeightDecider = mixCmp($boxA->getEmptyWeight(),$boxB->getEmptyWeight()); // with smallest empty weight
        if ($emptyWeightDecider !== 0) {
            return $emptyWeightDecider;
        }

        // maximum weight capacity as fallback decider
        return mixCmp(($boxA->getMaxWeight() - $boxA->getEmptyWeight()),($boxB->getMaxWeight() - $boxB->getEmptyWeight()));
    }
}
