<?php

declare(strict_types=1);

namespace CliqThemes\UI\Controls;

/**
 * Evaluates the portable visibility conditions used by PHP-defined controls.
 *
 * Conditions are intentionally data-only so the same structure can be
 * evaluated by PHP and JavaScript:
 *
 * [
 *     'all' => [
 *         ['path' => 'mode', 'operator' => 'equals', 'value' => 'layout'],
 *         ['path' => 'display', 'operator' => 'in', 'value' => ['grid', 'list']],
 *     ],
 * ]
 */
final class ConditionEvaluator
{
    /**
     * @param array<string, mixed> $condition
     * @param array<string, mixed> $context
     */
    public static function matches(array $condition, array $context): bool
    {
        if ($condition === []) {
            return true;
        }

        if (isset($condition['all']) && is_array($condition['all'])) {
            foreach ($condition['all'] as $child) {
                if (! is_array($child) || ! self::matches($child, $context)) {
                    return false;
                }
            }

            return true;
        }

        if (isset($condition['any']) && is_array($condition['any'])) {
            foreach ($condition['any'] as $child) {
                if (is_array($child) && self::matches($child, $context)) {
                    return true;
                }
            }

            return false;
        }

        if (isset($condition['not']) && is_array($condition['not'])) {
            return ! self::matches($condition['not'], $context);
        }

        $path = isset($condition['path']) ? (string) $condition['path'] : '';
        if ($path === '') {
            return false;
        }

        $actual = self::valueAtPath($context, $path);
        $expected = $condition['value'] ?? null;
        $operator = isset($condition['operator']) ? (string) $condition['operator'] : 'equals';

        return match ($operator) {
            'equals' => $actual === $expected,
            'not_equals', 'notEquals' => $actual !== $expected,
            'in' => is_array($expected) && in_array($actual, $expected, true),
            'not_in', 'notIn' => is_array($expected) && ! in_array($actual, $expected, true),
            'truthy' => (bool) $actual,
            'falsy' => ! (bool) $actual,
            'exists' => $actual !== null,
            'greater_than', 'greaterThan' => is_numeric($actual)
                && is_numeric($expected)
                && (float) $actual > (float) $expected,
            'less_than', 'lessThan' => is_numeric($actual)
                && is_numeric($expected)
                && (float) $actual < (float) $expected,
            default => false,
        };
    }

    /**
     * @param array<string, mixed> $source
     */
    public static function valueAtPath(array $source, string $path): mixed
    {
        $value = $source;

        foreach (explode('.', $path) as $segment) {
            if (! is_array($value) || ! array_key_exists($segment, $value)) {
                return null;
            }

            $value = $value[$segment];
        }

        return $value;
    }
}
