<?php

namespace MmNijas\Strftime;

use DateTime;
use DateTimeZone;
use DateTimeInterface;
use Exception;
use IntlDateFormatter;
use IntlGregorianCalendar;
use InvalidArgumentException;
use Locale;

class Strftime
{
    /**
     * Locale-formatted strftime using IntlDateFormatter (PHP 8.1 compatible).
     * Provides a cross-platform alternative to strftime().
     *
     * @param string $format
     * @param mixed $timestamp
     * @param string|null $locale
     * @return string
     */
    public static function strftime(string $format, $timestamp = null, ?string $locale = null): string
    {
        if (!($timestamp instanceof DateTimeInterface)) {
            $timestamp = is_int($timestamp) ? '@' . $timestamp : (string) $timestamp;

            try {
                $timestamp = new DateTime($timestamp);
            } catch (Exception $e) {
                throw new InvalidArgumentException('$timestamp argument is not valid.', 0, $e);
            }

            $timestamp->setTimezone(new DateTimeZone(date_default_timezone_get()));
        }

        $locale = Locale::canonicalize($locale ?? (Locale::getDefault() ?? setlocale(LC_TIME, '0')));

        $intl_formats = [
            '%a' => 'ccc',
            '%A' => 'EEEE',
            '%b' => 'LLL',
            '%B' => 'MMMM',
            '%h' => 'MMM',
        ];

        $intl_formatter = function (DateTimeInterface $timestamp, string $format) use ($intl_formats, $locale) {
            $tz = $timestamp->getTimezone();
            $date_type = IntlDateFormatter::FULL;
            $time_type = IntlDateFormatter::FULL;
            $pattern = '';

            switch ($format) {
                case '%c':
                    $date_type = IntlDateFormatter::LONG;
                    $time_type = IntlDateFormatter::SHORT;
                    break;
                case '%x':
                    $date_type = IntlDateFormatter::SHORT;
                    $time_type = IntlDateFormatter::NONE;
                    break;
                case '%X':
                    $date_type = IntlDateFormatter::NONE;
                    $time_type = IntlDateFormatter::MEDIUM;
                    break;
                default:
                    $pattern = $intl_formats[$format];
            }

            $calendar = IntlGregorianCalendar::createInstance();
            if ($calendar instanceof IntlGregorianCalendar) {
                $calendar->setGregorianChange(PHP_INT_MIN);
            }

            return (new IntlDateFormatter($locale, $date_type, $time_type, $tz, $calendar, $pattern))->format($timestamp);
        };

        $translation_table = [
            '%a' => $intl_formatter,
            '%A' => $intl_formatter,
            '%d' => 'd',
            '%e' => fn($timestamp) => sprintf('% 2u', $timestamp->format('j')),
            '%j' => fn($timestamp) => sprintf('%03d', $timestamp->format('z') + 1),
            '%u' => 'N',
            '%w' => 'w',
            '%U' => fn($timestamp) => sprintf('%02u', 1 + ($timestamp->format('z') - (new DateTime(sprintf('%d-01 Sunday', $timestamp->format('Y'))))->format('z')) / 7),
            '%V' => 'W',
            '%W' => fn($timestamp) => sprintf('%02u', 1 + ($timestamp->format('z') - (new DateTime(sprintf('%d-01 Monday', $timestamp->format('Y'))))->format('z')) / 7),
            '%b' => $intl_formatter,
            '%B' => $intl_formatter,
            '%h' => $intl_formatter,
            '%m' => 'm',
            '%C' => fn($timestamp) => floor($timestamp->format('Y') / 100),
            '%g' => fn($timestamp) => substr($timestamp->format('o'), -2),
            '%G' => 'o',
            '%y' => 'y',
            '%Y' => 'Y',
            '%H' => 'H',
            '%k' => fn($timestamp) => sprintf('% 2u', $timestamp->format('G')),
            '%I' => 'h',
            '%l' => fn($timestamp) => sprintf('% 2u', $timestamp->format('g')),
            '%M' => 'i',
            '%p' => 'A',
            '%P' => 'a',
            '%r' => 'h:i:s A',
            '%R' => 'H:i',
            '%S' => 's',
            '%T' => 'H:i:s',
            '%X' => $intl_formatter,
            '%z' => 'O',
            '%Z' => 'T',
            '%c' => $intl_formatter,
            '%D' => 'm/d/Y',
            '%F' => 'Y-m-d',
            '%s' => 'U',
            '%x' => $intl_formatter,
        ];

        $out = preg_replace_callback('/(?<!%)%([_#-]?)([a-zA-Z])/', function ($match) use ($translation_table, $timestamp) {
            $prefix = $match[1];
            $char = $match[2];
            $pattern = '%' . $char;

            if (!isset($translation_table[$pattern])) {
                throw new InvalidArgumentException("Format $pattern is unknown.");
            }

            $replace = $translation_table[$pattern];
            $result = is_string($replace) ? $timestamp->format($replace) : $replace($timestamp, $pattern);

            return match ($prefix) {
                '_' => preg_replace('/\G0(?=.)/', ' ', $result),
                '#', '-' => preg_replace('/^[0\s]+(?=.)/', '', $result),
                default => $result,
            };
        }, $format);

        return str_replace('%%', '%', $out);
    }
}