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

use ArrayIterator;
use function count;
use Countable;
use IteratorAggregate;
use JsonSerializable;
use function reset;
use function round;
use Traversable;
use function usort;

/**
 * List of packed boxes.
 *
 * @author Doug Wright
 */
class PackedBoxList implements IteratorAggregate, Countable, JsonSerializable
{
    /**
     * List containing boxes.
     *
     * @var PackedBox[]
     * @todo D5W: CHANGED private TO protected
     */
     protected $list = [];

    /**
     * Has this list already been sorted?
     *
     * @var bool
     * @todo D5W: CHANGED private TO protected
     */
    protected $isSorted = false;

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

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

    /**
     * Number of items in list.
     * @return int
     */
    #[\ReturnTypeWillChange]
    public function count()
    {
        return count($this->list);
    }

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

    /**
     * Do a bulk insert.
     *
     * @internal
     *
     * @param PackedBox[] $boxes
     * @return void
     */
    public function insertFromArray(array $boxes)
    {
        foreach ($boxes as $box) {
            $this->insert($box);
        }
    }

    /**
     * @internal
     * @return PackedBox
     */
    public function top()
    {
        if (!$this->isSorted) {
            usort($this->list, [$this, 'compare']);
            $this->isSorted = true;
        }

        return reset($this->list);
    }

    /**
     * @param PackedBox $boxA
     * @param PackedBox $boxB
     * @return int
     */
    public function compare(PackedBox $boxA, PackedBox $boxB)
    {
        $choice = mixCmp($boxB->getItems()->count(), $boxA->getItems()->count());
        if ($choice === 0) {
            $choice = mixCmp($boxB->getInnerVolume(), $boxA->getInnerVolume());
        }
        if ($choice === 0) {
            $choice = mixCmp($boxA->getWeight(), $boxB->getWeight());
        }

        return $choice;
    }

    /**
     * Calculate the average (mean) weight of the boxes.
     * @return float
     */
    public function getMeanWeight()
    {
        $meanWeight = 0;

        /** @var PackedBox $box */
        foreach ($this->list as $box) {
            $meanWeight += $box->getWeight();
        }

        return $meanWeight / count($this->list);
    }

    /**
     * Calculate the average (mean) weight of the boxes.
     * @return float
     */
    public function getMeanItemWeight()
    {
        $meanWeight = 0;

        /** @var PackedBox $box */
        foreach ($this->list as $box) {
            $meanWeight += $box->getItemWeight();
        }

        return $meanWeight / count($this->list);
    }

    /**
     * Calculate the variance in weight between these boxes.
     * @return float
     */
    public function getWeightVariance()
    {
        $mean = $this->getMeanWeight();

        $weightVariance = 0;
        /** @var PackedBox $box */
        foreach ($this->list as $box) {
            $weightVariance += ($box->getWeight() - $mean) ** 2;
        }

        return round($weightVariance / count($this->list), 1);
    }

    /**
     * Get volume utilisation of the set of packed boxes.
     * @return float
     */
    public function getVolumeUtilisation()
    {
        $itemVolume = 0;
        $boxVolume = 0;

        /** @var PackedBox $box */
        foreach ($this as $box) {
            $boxVolume += $box->getInnerVolume();

            /** @var PackedItem $item */
            foreach ($box->getItems() as $item) {
                $itemVolume += ($item->getItem()->getWidth() * $item->getItem()->getLength() * $item->getItem()->getDepth());
            }
        }

        return round($itemVolume / $boxVolume * 100, 1);
    }

    /**
     * @return array
     */
    #[\ReturnTypeWillChange]
    public function jsonSerialize()
    {
        return $this->list;
    }
}
