<?php

/**
 * Box packing (3D bin packing, knapsack problem).
 *
 * @author Doug Wright
 */
declare (strict_types=1);
namespace FlexibleShippingUspsVendor\DVDoug\BoxPacker;

use FlexibleShippingUspsVendor\Psr\Log\LoggerAwareInterface;
use FlexibleShippingUspsVendor\Psr\Log\LoggerInterface;
use FlexibleShippingUspsVendor\Psr\Log\NullLogger;
use function array_filter;
use function count;
use function usort;
/**
 * Figure out orientations for an item and a given set of dimensions.
 *
 * @internal
 */
class OrientatedItemFactory implements LoggerAwareInterface
{
    protected LoggerInterface $logger;
    protected Box $box;
    /**
     * Whether the packer is in single-pass mode.
     */
    protected bool $singlePassMode = \false;
    protected bool $boxIsRotated = \false;
    /**
     * @var array<string, bool>
     */
    protected static array $emptyBoxStableItemOrientationCache = [];
    public function __construct(Box $box)
    {
        $this->box = $box;
        $this->logger = new NullLogger();
    }
    public function setLogger(LoggerInterface $logger): void
    {
        $this->logger = $logger;
    }
    public function setSinglePassMode(bool $singlePassMode): void
    {
        $this->singlePassMode = $singlePassMode;
    }
    public function setBoxIsRotated(bool $boxIsRotated): void
    {
        $this->boxIsRotated = $boxIsRotated;
    }
    /**
     * Get the best orientation for an item.
     */
    public function getBestOrientation(Item $item, ?OrientatedItem $prevItem, ItemList $nextItems, int $widthLeft, int $lengthLeft, int $depthLeft, int $rowLength, int $x, int $y, int $z, PackedItemList $prevPackedItemList, bool $considerStability): ?OrientatedItem
    {
        $this->logger->debug("evaluating item {$item->getDescription()} for fit", ['item' => $item, 'space' => ['widthLeft' => $widthLeft, 'lengthLeft' => $lengthLeft, 'depthLeft' => $depthLeft], 'position' => ['x' => $x, 'y' => $y, 'z' => $z]]);
        $possibleOrientations = $this->getPossibleOrientations($item, $prevItem, $widthLeft, $lengthLeft, $depthLeft, $x, $y, $z, $prevPackedItemList);
        $usableOrientations = $considerStability ? $this->getUsableOrientations($item, $possibleOrientations) : $possibleOrientations;
        if (empty($usableOrientations)) {
            return null;
        }
        $sorter = new OrientatedItemSorter($this, $this->singlePassMode, $widthLeft, $lengthLeft, $depthLeft, $nextItems, $rowLength, $x, $y, $z, $prevPackedItemList, $this->logger);
        usort($usableOrientations, $sorter);
        $this->logger->debug('Selected best fit orientation', ['orientation' => $usableOrientations[0]]);
        return $usableOrientations[0];
    }
    /**
     * Find all possible orientations for an item.
     *
     * @return OrientatedItem[]
     */
    public function getPossibleOrientations(Item $item, ?OrientatedItem $prevItem, int $widthLeft, int $lengthLeft, int $depthLeft, int $x, int $y, int $z, PackedItemList $prevPackedItemList): array
    {
        $permutations = $this->generatePermutations($item, $prevItem);
        // remove any that simply don't fit
        $orientations = [];
        foreach ($permutations as $dimensions) {
            if ($dimensions[0] <= $widthLeft && $dimensions[1] <= $lengthLeft && $dimensions[2] <= $depthLeft) {
                $orientations[] = new OrientatedItem($item, $dimensions[0], $dimensions[1], $dimensions[2]);
            }
        }
        if ($item instanceof ConstrainedPlacementItem && !$this->box instanceof WorkingVolume) {
            $orientations = array_filter($orientations, function (OrientatedItem $i) use ($x, $y, $z, $prevPackedItemList): bool {
                /** @var ConstrainedPlacementItem $constrainedItem */
                $constrainedItem = $i->getItem();
                if ($this->boxIsRotated) {
                    $rotatedPrevPackedItemList = new PackedItemList();
                    foreach ($prevPackedItemList as $prevPackedItem) {
                        $rotatedPrevPackedItemList->insert(new PackedItem($prevPackedItem->getItem(), $prevPackedItem->getY(), $prevPackedItem->getX(), $prevPackedItem->getZ(), $prevPackedItem->getLength(), $prevPackedItem->getWidth(), $prevPackedItem->getDepth()));
                    }
                    return $constrainedItem->canBePacked($this->box, $rotatedPrevPackedItemList, $y, $x, $z, $i->getLength(), $i->getWidth(), $i->getDepth());
                } else {
                    return $constrainedItem->canBePacked($this->box, $prevPackedItemList, $x, $y, $z, $i->getWidth(), $i->getLength(), $i->getDepth());
                }
            });
        }
        return $orientations;
    }
    /**
     * @param  OrientatedItem[] $possibleOrientations
     * @return OrientatedItem[]
     */
    protected function getUsableOrientations(Item $item, array $possibleOrientations): array
    {
        $stableOrientations = $unstableOrientations = [];
        // Divide possible orientations into stable (low centre of gravity) and unstable (high centre of gravity)
        foreach ($possibleOrientations as $orientation) {
            if ($orientation->isStable() || $this->box->getInnerDepth() === $orientation->getDepth()) {
                $stableOrientations[] = $orientation;
            } else {
                $unstableOrientations[] = $orientation;
            }
        }
        /*
         * We prefer to use stable orientations only, but allow unstable ones if
         * the item doesn't fit in the box any other way
         */
        if (count($stableOrientations) > 0) {
            return $stableOrientations;
        }
        if (count($unstableOrientations) > 0 && !$this->hasStableOrientationsInEmptyBox($item)) {
            return $unstableOrientations;
        }
        return [];
    }
    /**
     * Return the orientations for this item if it were to be placed into the box with nothing else.
     */
    protected function hasStableOrientationsInEmptyBox(Item $item): bool
    {
        $cacheKey = $item->getWidth() . '|' . $item->getLength() . '|' . $item->getDepth() . '|' . ($item->getKeepFlat() ? '2D' : '3D') . '|' . $this->box->getInnerWidth() . '|' . $this->box->getInnerLength() . '|' . $this->box->getInnerDepth();
        if (isset(static::$emptyBoxStableItemOrientationCache[$cacheKey])) {
            return static::$emptyBoxStableItemOrientationCache[$cacheKey];
        }
        $orientations = $this->getPossibleOrientations($item, null, $this->box->getInnerWidth(), $this->box->getInnerLength(), $this->box->getInnerDepth(), 0, 0, 0, new PackedItemList());
        $stableOrientations = array_filter($orientations, static fn(OrientatedItem $orientation) => $orientation->isStable());
        static::$emptyBoxStableItemOrientationCache[$cacheKey] = count($stableOrientations) > 0;
        return static::$emptyBoxStableItemOrientationCache[$cacheKey];
    }
    /**
     * @return array<array<int>>
     */
    private function generatePermutations(Item $item, ?OrientatedItem $prevItem): array
    {
        // Special case items that are the same as what we just packed - keep orientation
        if ($prevItem && $prevItem->isSameDimensions($item)) {
            return [[$prevItem->getWidth(), $prevItem->getLength(), $prevItem->getDepth()]];
        }
        $permutations = [];
        $w = $item->getWidth();
        $l = $item->getLength();
        $d = $item->getDepth();
        // simple 2D rotation
        $permutations[$w . '|' . $l . '|' . $d] = [$w, $l, $d];
        $permutations[$l . '|' . $w . '|' . $d] = [$l, $w, $d];
        // add 3D rotation if we're allowed
        if (!$item->getKeepFlat()) {
            $permutations[$w . '|' . $d . '|' . $l] = [$w, $d, $l];
            $permutations[$l . '|' . $d . '|' . $w] = [$l, $d, $w];
            $permutations[$d . '|' . $w . '|' . $l] = [$d, $w, $l];
            $permutations[$d . '|' . $l . '|' . $w] = [$d, $l, $w];
        }
        return $permutations;
    }
}
