<?php
/**
 * Copyright 2023 Google Inc. All Rights Reserved.
 *
 * 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;

use \Google\Protobuf\Internal\Message;
use Google\ApiCore\ApiException;
use Google\ApiCore\OperationResponse;
use Google\ApiCore\Serializer;
use Google\Cloud\Core\Exception\NotFoundException;
use Google\Cloud\Core\Exception\ServiceException;

/**
 * @internal
 * Responsible for forwarding the requests to their
 * respective client methdos via the request wrapper.
 */
class RequestHandler
{
    use EmulatorTrait;
    use ArrayTrait;
    use TimeTrait;
    use WhitelistTrait;
    use RequestProcessorTrait;

    /**
     * @var Serializer
     */
    private Serializer $serializer;

    private array $clients = [];

    /**
     * @param Serializer $serializer
     * @param array<string|object> $clients
     * @param array $clientConfig
     */
    public function __construct(
        Serializer $serializer,
        array $clients,
        array $clientConfig = []
    ) {
        //@codeCoverageIgnoreStart
        $this->serializer = $serializer;
        $clientConfig['serializer'] = $serializer;

        // Adds some defaults
        // gccl needs to be present for handwritten clients
        $clientConfig += [
            'libName' => 'gccl',
            'emulatorHost' => null
        ];

        if ((bool) $clientConfig['emulatorHost']) {
            $emulatorConfig = $this->emulatorGapicConfig($clientConfig['emulatorHost']);
            $clientConfig = array_merge(
                $clientConfig,
                $emulatorConfig
            );
        }
        //@codeCoverageIgnoreEnd

        // Initialize the client classes and store them in memory
        foreach ($clients as $client) {
            if (is_object($client)) {
                $this->clients[get_class($client)] = $client;
            } else {
                $this->clients[$client] = new $client($clientConfig);
            }
        }
    }

    /**
     * Helper function that forwards the request to a client obj.
     *
     * @param string $clientClass The request will be forwarded to this client class.
     * @param string $method This method needs to be called on the client obj.
     * @param Message $request The protobuf Request instance to pass as the first argument to the $method.
     * @param array $optionalArgs The optional args.
     * @param bool $whitelisted This decides the behaviour when a NotFoundException is encountered.
     *
     * @return \Generator|OperationResponse|array|null
     *
     * @throws ServiceException
     */
    public function sendRequest(
        string $clientClass,
        string $method,
        Message $request,
        array $optionalArgs,
        bool $whitelisted = false
    ) {
        $clientObj = $this->getClientObject($clientClass);

        if (!$clientObj) {
            return null;
        }

        $allArgs = [$request];
        $allArgs[] = $optionalArgs;

        try {
            $callable = [$clientObj, $method];
            $response = call_user_func_array($callable, $allArgs);

            return $this->handleResponse($response);
        } catch (ApiException $ex) {
            throw $this->convertToGoogleException($ex);
        } catch (NotFoundException $e) {
            if ($whitelisted) {
                throw $this->modifyWhitelistedError($e);
            }

            throw $e;
        }
    }

    /**
     * Helper function that returns a client object stored in memory
     * using the client class as key.
     * @param $clientClass The client class whose object we need.
     * @return mixed
     */
    private function getClientObject(string $clientClass)
    {
        return $this->clients[$clientClass] ?? null;
    }
}
