<?php
/**
 * Copyright 2017 Google Inc.
 *
 * Licensed 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.
 */

namespace Google\Cloud\Core\Testing\System;

use Google\ApiCore\ApiException;
use Google\Cloud\Core\Exception\NotFoundException;
use Google\Cloud\Core\ExponentialBackoff;

/**
 * Manage a queue of items to be cleaned up at the end of the test run.
 *
 * @experimental
 * @internal
 */
class DeletionQueue
{
    /**
     * @var callable[]
     */
    private $queue = [];

    /**
     * @var bool
     */
    private $acceptAllInputs;

    /**
     * @param bool $acceptAllInputs If false, only callables or objects with
     *        `delete` methods are allowed. **Defaults to** `false`.
     *
     * @experimental
     * @internal
     */
    public function __construct($acceptAllInputs = false)
    {
        $this->acceptAllInputs = $acceptAllInputs;
    }

    /**
     * Add an item to be cleaned up.
     *
     * @param mixed $toDelete Unless the class was created with
     *        `$acceptAllInputs = true`, either a callable with no arguments, or
     *        an object with a `delete` method.
     * @return void
     *
     * @experimental
     * @internal
     */
    public function add($toDelete)
    {
        if (!$this->acceptAllInputs) {
            if (!is_callable($toDelete) && !method_exists($toDelete, 'delete')) {
                throw new \BadMethodCallException(
                    'Deletion Queue requires a callable, or an object with a `delete` method.'
                );
            }

            if (!is_callable($toDelete)) {
                $toDelete = function () use ($toDelete) {
                    $toDelete->delete();
                };
            }
        }

        $this->queue[] = $toDelete;
    }

    /**
     * Process all items in the deletion queue.
     *
     * @return void
     * @throws ApiException
     *
     * @experimental
     * @internal
     */
    public function process(?callable $action = null)
    {
        if ($action) {
            $action($this->queue);
        } else {
            $backoff = new ExponentialBackoff(8);

            foreach ($this->queue as $item) {
                $backoff->execute(function () use ($item) {
                    try {
                        call_user_func($item);
                    } catch (NotFoundException $e) {
                    } catch (ApiException $apiException) {
                        if ($apiException->getStatus() !== 'NOT_FOUND') {
                            throw $apiException;
                        }
                    }
                });
            }
        }
    }
}
