<?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\ApiCore\ArrayTrait;
use Google\ApiCore\Options\CallOptions;
use Google\Protobuf\Internal\Message;
use Google\Protobuf\NullValue;
use LogicException;

/**
 * @internal
 * Supplies helper methods to interact with the APIs.
 */
trait ApiHelperTrait
{
    use ArrayTrait;
    use TimeTrait;

    private OptionsValidator $optionsValidator;

    /**
     * Format a struct for the API.
     *
     * @param array $fields
     * @return array
     */
    private function formatStructForApi(array $fields)
    {
        $fFields = [];

        foreach ($fields as $key => $value) {
            $fFields[$key] = $this->formatValueForApi($value);
        }

        return ['fields' => $fFields];
    }

    private function unpackStructFromApi(array $struct)
    {
        $vals = [];
        foreach ($struct['fields'] as $key => $val) {
            $vals[$key] = $this->unpackValue($val);
        }
        return $vals;
    }

    private function unpackValue($value)
    {
        if (count($value) > 1) {
            throw new \RuntimeException("Unexpected fields in struct: $value");
        }

        foreach ($value as $setField => $setValue) {
            switch ($setField) {
                case 'listValue':
                    $valueList = [];
                    foreach ($setValue['values'] as $innerValue) {
                        $valueList[] = $this->unpackValue($innerValue);
                    }
                    return $valueList;
                case 'structValue':
                    return $this->unpackStructFromApi($value['structValue']);
                default:
                    return $setValue;
            }
        }
    }

    private function flattenStruct(array $struct)
    {
        return $struct['fields'];
    }

    private function flattenValue(array $value)
    {
        if (count($value) > 1) {
            throw new \RuntimeException("Unexpected fields in struct: $value");
        }

        if (isset($value['nullValue'])) {
            return null;
        }

        return array_pop($value);
    }

    private function flattenListValue(array $value)
    {
        return $value['values'];
    }

    /**
     * Format a list for the API.
     *
     * @param array $list
     * @return array
     */
    private function formatListForApi(array $list)
    {
        $values = [];

        foreach ($list as $value) {
            $values[] = $this->formatValueForApi($value);
        }

        return ['values' => $values];
    }

    /**
     * Format a value for the API.
     *
     * @param mixed $value
     * @return array
     */
    private function formatValueForApi($value)
    {
        $type = gettype($value);

        switch ($type) {
            case 'string':
                return ['string_value' => $value];
            case 'double':
            case 'integer':
                return ['number_value' => $value];
            case 'boolean':
                return ['bool_value' => $value];
            case 'NULL':
                return ['null_value' => NullValue::NULL_VALUE];
            case 'array':
                if (!empty($value) && $this->isAssoc($value)) {
                    return ['struct_value' => $this->formatStructForApi($value)];
                }

                return ['list_value' => $this->formatListForApi($value)];
        }

        return [];
    }

    /**
     * Format a gRPC timestamp to match the format returned by the REST API.
     *
     * @param array $timestamp
     * @return string
     */
    private function formatTimestampFromApi(array $timestamp)
    {
        $timestamp += [
            'seconds' => 0,
            'nanos' => 0
        ];

        $dt = $this->createDateTimeFromSeconds($timestamp['seconds']);

        return $this->formatTimeAsString($dt, $timestamp['nanos']);
    }

    /**
     * Format a timestamp for the API with nanosecond precision.
     *
     * @param string $value
     * @return array
     */
    private function formatTimestampForApi($value)
    {
        list($dt, $nanos) = $this->parseTimeString($value);

        return [
            'seconds' => (int) $dt->format('U'),
            'nanos' => (int) $nanos
        ];
    }

    /**
     * Format a duration for the API.
     *
     * @param string|mixed $value
     * @return array
     */
    private function formatDurationForApi($value)
    {
        if (is_string($value)) {
            $d = explode('.', trim($value, 's'));
            if (count($d) < 2) {
                $seconds = $d[0];
                $nanos = 0;
            } else {
                $seconds = (int) $d[0];
                $nanos = $this->convertFractionToNanoSeconds($d[1]);
            }
        } elseif ($value instanceof Duration) {
            $d = $value->get();
            $seconds = $d['seconds'];
            $nanos = $d['nanos'];
        }

        return [
            'seconds' => $seconds,
            'nanos' => $nanos
        ];
    }

    /**
     * Construct a gapic client. Allows for tests to intercept.
     *
     * @param string $gapicName
     * @param array $config
     * @return mixed
     */
    protected function constructGapic($gapicName, array $config)
    {
        return new $gapicName($config);
    }

    /**
     * Helper function to convert selective elements into protos out of a given input array.
     *
     * Example:
     * ```
     * $output = $topic->convertDataToProtos(['schema' =>[], 'other vals'], ['schema' => Schema::class]);
     * $output['schema']; // This will be of the Schema type.
     * ```
     *
     * @param array $input The input array.
     * @param array $map The key,value pairs specifying the elements and the proto classes.
     *
     * @return array The modified array
     */
    private function convertDataToProtos(array $input, array $map): array
    {
        if (!isset($this->serializer)) {
            throw new \LogicException('Serializer must be set to use this function');
        }
        foreach ($map as $key => $className) {
            if (isset($input[$key])) {
                $input[$key] = $this->serializer->decodeMessage(new $className(), $input[$key]);
            }
        }

        return $input;
    }

    /**
     * Helper method used to split a supplied set of options into parameters that are passed into
     * a proto message and optional args.
     * We strictly treat the parameters allowed by `CallOptions` in GAX as the optional params
     * and everything else that is passed is passed to the Proto message constructor.
     */
    private function splitOptionalArgs(array $input, array $extraAllowedKeys = []): array
    {
        $callOptionFields = array_keys((new CallOptions([]))->toArray());
        $keys = array_merge($callOptionFields, $extraAllowedKeys);

        $callOptions = $this->pluckArray($keys, $input);

        return [$input, $callOptions];
    }

    /**
     * Helper method used to validate optons based on the supplied $optionTypes
     * $optionTypes can be an array of string keys, a protobuf Message classname, or a
     * the CallOptions classname. Parameters are split and returned in the order
     * that the options types are provided.
     *
     * @throws LogicException
     */
    private function validateOptions(array $options, array|Message|string ...$optionTypes): array
    {
        if (!isset($this->optionsValidator)) {
            $this->optionsValidator = new OptionsValidator();
        }
        return $this->optionsValidator->validateOptions($options, ...$optionTypes);
    }
}
