<?php
declare(strict_types=1);

/**
 * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
 * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
 *
 * Licensed under The MIT License
 * Redistributions of files must retain the above copyright notice.
 *
 * @copyright     Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
 * @link          https://cakephp.org CakePHP(tm) Project
 * @since         3.0.0
 * @license       https://opensource.org/licenses/mit-license.php MIT License
 */
namespace Cake\Utility;

/**
 * Provides features for merging object properties recursively with
 * parent classes.
 */
trait MergeVariablesTrait
{
    /**
     * Merge the list of $properties with all parent classes of the current class.
     *
     * ### Options:
     *
     * - `associative` - A list of properties that should be treated as associative arrays.
     *   Properties in this list will be passed through Hash::normalize() before merging.
     *
     * @param array<string> $properties An array of properties and the merge strategy for them.
     * @param array<string, mixed> $options The options to use when merging properties.
     * @return void
     */
    protected function _mergeVars(array $properties, array $options = []): void
    {
        $class = static::class;
        $parents = [];
        while (true) {
            $parent = get_parent_class($class);
            if (!$parent) {
                break;
            }
            $parents[] = $parent;
            $class = $parent;
        }
        foreach ($properties as $property) {
            if (!property_exists($this, $property)) {
                continue;
            }
            $thisValue = $this->{$property};
            if ($thisValue === null || $thisValue === false) {
                continue;
            }
            $this->_mergeProperty($property, $parents, $options);
        }
    }

    /**
     * Merge a single property with the values declared in all parent classes.
     *
     * @param string $property The name of the property being merged.
     * @param array<string> $parentClasses An array of classes you want to merge with.
     * @param array<string, mixed> $options Options for merging the property, see _mergeVars()
     * @return void
     */
    protected function _mergeProperty(string $property, array $parentClasses, array $options): void
    {
        $thisValue = $this->{$property};
        $isAssoc = false;
        if (
            isset($options['associative']) &&
            in_array($property, (array)$options['associative'], true)
        ) {
            $isAssoc = true;
        }

        if ($isAssoc) {
            $thisValue = Hash::normalize($thisValue);
        }
        foreach ($parentClasses as $class) {
            $parentProperties = get_class_vars($class);
            if (empty($parentProperties[$property])) {
                continue;
            }
            $parentProperty = $parentProperties[$property];
            if (!is_array($parentProperty)) {
                continue;
            }
            $thisValue = $this->_mergePropertyData($thisValue, $parentProperty, $isAssoc);
        }
        $this->{$property} = $thisValue;
    }

    /**
     * Merge each of the keys in a property together.
     *
     * @param array $current The current merged value.
     * @param array $parent The parent class' value.
     * @param bool $isAssoc Whether the merging should be done in associative mode.
     * @return array The updated value.
     */
    protected function _mergePropertyData(array $current, array $parent, bool $isAssoc): array
    {
        if (!$isAssoc) {
            return array_merge($parent, $current);
        }
        $parent = Hash::normalize($parent);
        foreach ($parent as $key => $value) {
            $current[$key] ??= $value;
        }

        return $current;
    }
}
