<?php

namespace Timber;

use DateInterval;
use DateTime;
use DateTimeInterface;
use Exception;
use Timber\Factory\PostFactory;
use Timber\Factory\TermFactory;
use Twig\Environment;
use Twig\Extension\CoreExtension;
use Twig\Extension\EscaperExtension;
use Twig\Runtime\EscaperRuntime;
use Twig\TwigFilter;
use Twig\TwigFunction;
use Twig\DeprecatedCallableInfo;

/**
 * Class Twig
 */
class Twig
{
    public static $dir_name;

    /**
     * @codeCoverageIgnore
     */
    public static function init(): void
    {
        $self = new self();

        \add_filter('timber/twig', [$self, 'add_timber_functions']);
        \add_filter('timber/twig', [$self, 'add_timber_filters']);
        \add_filter('timber/twig', [$self, 'add_timber_escaper_filters']);
        \add_filter('timber/twig', [$self, 'add_timber_escapers']);
        \add_filter('timber/loader/twig', [$self, 'set_defaults']);
    }

    /**
     * Get Timber default functions
     *
     * @return array Default Timber functions
     */
    public function get_timber_functions()
    {
        $post_factory = new PostFactory();
        $termFactory = new TermFactory();

        $functions = [
            'action' => [
                'callable' => function ($action_name, ...$args) {
                    \do_action_ref_array($action_name, $args);
                },
            ],
            'function' => [
                'callable' => [$this, 'exec_function'],
            ],
            'fn' => [
                'callable' => [$this, 'exec_function'],
            ],
            'get_post' => [
                'callable' => [Timber::class, 'get_post'],
            ],
            'get_image' => [
                'callable' => [Timber::class, 'get_image'],
            ],
            'get_external_image' => [
                'callable' => [Timber::class, 'get_external_image'],
            ],
            'get_attachment' => [
                'callable' => [Timber::class, 'get_attachment'],
            ],
            'get_posts' => [
                'callable' => [Timber::class, 'get_posts'],
            ],
            'get_attachment_by' => [
                'callable' => [Timber::class, 'get_attachment_by'],
            ],
            'get_term' => [
                'callable' => [Timber::class, 'get_term'],
            ],
            'get_terms' => [
                'callable' => [Timber::class, 'get_terms'],
            ],
            'get_user' => [
                'callable' => [Timber::class, 'get_user'],
            ],
            'get_users' => [
                'callable' => [Timber::class, 'get_users'],
            ],
            'get_comment' => [
                'callable' => [Timber::class, 'get_comment'],
            ],
            'get_comments' => [
                'callable' => [Timber::class, 'get_comments'],
            ],
            'Post' => [
                'callable' => function ($post_id) use ($post_factory) {
                    Helper::deprecated('{{ Post() }}', '{{ get_post() }} or {{ get_posts() }}', '2.0.0');
                    return $post_factory->from($post_id);
                },
                'options' => [
                    'deprecation_info' => new DeprecatedCallableInfo(
                        package: 'timber/timber',
                        version: '2.0.0',
                        altName: '{{ get_post() }} or {{ get_posts() }}',
                        altPackage: null,
                        altVersion: null
                    ),
                ],
            ],
            'TimberPost' => [
                'callable' => function ($post_id) use ($post_factory) {
                    Helper::deprecated('{{ TimberPost() }}', '{{ get_post() }} or {{ get_posts() }}', '2.0.0');
                    return $post_factory->from($post_id);
                },
                'options' => [
                    'deprecation_info' => new DeprecatedCallableInfo(
                        package: 'timber/timber',
                        version: '2.0.0',
                        altName: '{{ get_post() }} or {{ get_posts() }}',
                        altPackage: null,
                        altVersion: null
                    ),
                ],
            ],
            'Image' => [
                'callable' => function ($post_id) use ($post_factory) {
                    Helper::deprecated('{{ Image() }}', '{{ get_image() }}, {{ get_post() }}, {{ get_posts() }}, {{ get_attachment() }} or {{ get_attachment_by() }}', '2.0.0');
                    return $post_factory->from($post_id);
                },
                'options' => [
                    'deprecation_info' => new DeprecatedCallableInfo(
                        package: 'timber/timber',
                        version: '2.0.0',
                        altName: '{{ get_image() }}',
                        altPackage: null,
                        altVersion: null
                    ),
                ],
            ],
            'TimberImage' => [
                'callable' => function ($post_id) use ($post_factory) {
                    Helper::deprecated('{{ TimberImage() }}', '{{ get_image() }}, {{ get_post() }}, {{ get_posts() }}, {{ get_attachment() }} or {{ get_attachment_by() }}', '2.0.0');
                    return $post_factory->from($post_id);
                },
                'options' => [
                    'deprecation_info' => new DeprecatedCallableInfo(
                        package: 'timber/timber',
                        version: '2.0.0',
                        altName: '{{ get_image() }}',
                        altPackage: null,
                        altVersion: null
                    ),
                ],
            ],
            'Term' => [
                'callable' => function ($term_id) use ($termFactory) {
                    Helper::deprecated('{{ Term() }}', '{{ get_term() }} or {{ get_terms() }}', '2.0.0');
                    return $termFactory->from($term_id);
                },
                'options' => [
                    'deprecation_info' => new DeprecatedCallableInfo(
                        package: 'timber/timber',
                        version: '2.0.0',
                        altName: '{{ get_term() }}',
                        altPackage: null,
                        altVersion: null
                    ),
                ],
            ],
            'TimberTerm' => [
                'callable' => function ($term_id) use ($termFactory) {
                    Helper::deprecated('{{ TimberTerm() }}', '{{ get_term() }} or {{ get_terms() }}', '2.0.0');
                    return $termFactory->from($term_id);
                },
                'options' => [
                    'deprecation_info' => new DeprecatedCallableInfo(
                        package: 'timber/timber',
                        version: '2.0.0',
                        altName: '{{ get_term() }} or {{ get_terms() }}',
                        altPackage: null,
                        altVersion: null
                    ),
                ],
            ],
            'User' => [
                'callable' => function ($user_id) {
                    Helper::deprecated('{{ User() }}', '{{ get_user() }} or {{ get_users() }}', '2.0.0');
                    return Timber::get_user($user_id);
                },
                'options' => [
                    'deprecation_info' => new DeprecatedCallableInfo(
                        package: 'timber/timber',
                        version: '2.0.0',
                        altName: '{{ get_user() }} or {{ get_users() }}',
                        altPackage: null,
                        altVersion: null
                    ),
                ],
            ],
            'TimberUser' => [
                'callable' => function ($user_id) {
                    Helper::deprecated('{{ TimberUser() }}', '{{ get_user() }} or {{ get_users() }}', '2.0.0');
                    return Timber::get_user($user_id);
                },
                'options' => [
                    'deprecation_info' => new DeprecatedCallableInfo(
                        package: 'timber/timber',
                        version: '2.0.0',
                        altName: '{{ get_user() }} or {{ get_users() }}',
                        altPackage: null,
                        altVersion: null
                    ),
                ],
            ],
            'shortcode' => [
                'callable' => 'do_shortcode',
            ],
            'bloginfo' => [
                'callable' => 'bloginfo',
            ],

            // Translation functions.
            '__' => [
                'callable' => '__',
            ],
            'translate' => [
                'callable' => 'translate',
            ],
            '_e' => [
                'callable' => '_e',
            ],
            '_n' => [
                'callable' => '_n',
            ],
            '_x' => [
                'callable' => '_x',
            ],
            '_ex' => [
                'callable' => '_ex',
            ],
            '_nx' => [
                'callable' => '_nx',
            ],
            '_n_noop' => [
                'callable' => '_n_noop',
            ],
            '_nx_noop' => [
                'callable' => '_nx_noop',
            ],
            'translate_nooped_plural' => [
                'callable' => 'translate_nooped_plural',
            ],
        ];

        /**
         * Filters the functions that are added to Twig.
         *
         * The `$functions` array is an associative array with the filter name as a key and an
         * arguments array as the value. In the arguments array, you pass the function to call with
         * a `callable` entry.
         *
         * This is an alternative filter that you can use instead of adding your function in the
         * `timber/twig` filter.
         *
         * @api
         * @since 2.0.0
         * @example
         * ```php
         * add_filter( 'timber/twig/functions', function( $functions ) {
         *     // Add your own function.
         *     $functions['url_to_domain'] = [
         *         'callable' => 'url_to_domain',
         *     ];
         *
         *     // Replace a function.
         *     $functions['get_image'] = [
         *         'callable' => 'custom_image_get',
         *     ];
         *
         *     // Remove a function.
         *     unset( $functions['bloginfo'] );
         *
         *     return $functions;
         * } );
         * ```
         *
         * @param array $functions
         */
        $functions = \apply_filters('timber/twig/functions', $functions);

        return $functions;
    }

    /**
     * Adds Timber-specific functions to Twig.
     *
     * @param Environment $twig The Twig Environment.
     *
     * @return Environment
     */
    public function add_timber_functions($twig)
    {
        foreach ($this->get_timber_functions() as $name => $function) {
            $twig->addFunction(
                new TwigFunction(
                    $name,
                    $function['callable'],
                    $function['options'] ?? []
                )
            );
        }

        return $twig;
    }

    /**
     * Get Timber default filters
     *
     * @return array Default Timber filters
     */
    public function get_timber_filters()
    {
        $filters = [
            /* image filters */
            'resize' => [
                'callable' => [ImageHelper::class, 'resize'],
            ],
            'retina' => [
                'callable' => [ImageHelper::class, 'retina_resize'],
            ],
            'letterbox' => [
                'callable' => [ImageHelper::class, 'letterbox'],
            ],
            'tojpg' => [
                'callable' => [ImageHelper::class, 'img_to_jpg'],
            ],
            'towebp' => [
                'callable' => [ImageHelper::class, 'img_to_webp'],
            ],

            // Debugging filters.
            'get_class' => [
                'callable' => function ($obj) {
                    Helper::deprecated('{{ my_object | get_class }}', "{{ function('get_class', my_object) }}", '2.0.0');
                    return $obj::class;
                },
                'options' => [
                    'deprecation_info' => new DeprecatedCallableInfo(
                        package: 'timber/timber',
                        version: '2.0.0',
                        altName: "{{ function('get_class', my_object) }}",
                        altPackage: null,
                        altVersion: null
                    ),
                ],
            ],
            'print_r' => [
                'callable' => function ($arr) {
                    Helper::deprecated('{{ my_object | print_r }}', '{{ dump(my_object) }}', '2.0.0');
                    return \print_r($arr, true);
                },
                'options' => [
                    'deprecation_info' => new DeprecatedCallableInfo(
                        package: 'timber/timber',
                        version: '2.0.0',
                        altName: '{{ dump(my_object) }}',
                        altPackage: null,
                        altVersion: null
                    ),
                ],
            ],

            // Other filters.
            'stripshortcodes' => [
                'callable' => 'strip_shortcodes',
            ],
            'array' => [
                'callable' => [$this, 'to_array'],
            ],
            'excerpt' => [
                'callable' => 'wp_trim_words',
            ],
            'excerpt_chars' => [
                'callable' => [TextHelper::class, 'trim_characters'],
            ],
            'function' => [
                'callable' => [$this, 'exec_function'],
            ],
            'pretags' => [
                'callable' => [$this, 'twig_pretags'],
            ],
            'sanitize' => [
                'callable' => 'sanitize_title',
            ],
            'shortcodes' => [
                'callable' => 'do_shortcode',
            ],
            'wpautop' => [
                'callable' => 'wpautop',
            ],
            'list' => [
                'callable' => [$this, 'add_list_separators'],
            ],
            'pluck' => [
                'callable' => [Helper::class, 'pluck'],
            ],
            'wp_list_filter' => [
                'callable' => [Helper::class, 'wp_list_filter'],
            ],

            'relative' => [
                'callable' => fn ($link) => URLHelper::get_rel_url($link, true),
            ],

            /**
             * Date and Time filters.
             */
            'date' => [
                'callable' => [$this, 'twig_date_format_filter'],
                'options' => [
                    'needs_environment' => true,
                ],
            ],
            'time_ago' => [
                'callable' => [DateTimeHelper::class, 'time_ago'],
            ],
            'truncate' => [
                'callable' => fn ($text, $len) => TextHelper::trim_words($text, $len),
            ],

            // Numbers filters
            'size_format' => [
                'callable' => 'size_format',
            ],

            // Actions and filters.
            'apply_filters' => [
                'callable' => function () {
                    $args = \func_get_args();
                    $tag = \current(\array_splice($args, 1, 1));

                    return \apply_filters_ref_array($tag, $args);
                },
            ],
        ];

        /**
         * Filters the filters that are added to Twig.
         *
         * The `$filters` array is an associative array with the filter name as a key and an
         * arguments array as the value. In the arguments array, you pass the function to call with
         * a `callable` entry.
         *
         * This is an alternative filter that you can use instead of adding your filter in the
         * `timber/twig` filter.
         *
         * @api
         * @since 2.0.0
         * @example
         * ```php
         * add_filter( 'timber/twig/default_filters', function( $filters ) {
         *     // Add your own filter.
         *     $filters['price'] = [
         *         'callable' => 'format_price',
         *     ];
         *
         *     // Replace a filter.
         *     $filters['list'] = [
         *         'callable' => 'custom_list_filter',
         *     ];
         *
         *     // Remove a filter.
         *     unset( $filters['list'] );
         *
         *     return $filters;
         * } );
         * ```
         *
         * @param array $filters
         */
        $filters = \apply_filters('timber/twig/filters', $filters);

        return $filters;
    }

    /**
     * Get Timber default filters
     *
     * @return array Default Timber filters
     */
    public function get_timber_escaper_filters()
    {
        $escaper_filters = [
            'esc_url' => [
                'callable' => 'esc_url',
            ],
            'wp_kses' => [
                'callable' => 'wp_kses',
            ],
            'wp_kses_post' => [
                'callable' => 'wp_kses_post',
            ],
            'esc_attr' => [
                'callable' => 'esc_attr',
            ],
            'esc_html' => [
                'callable' => 'esc_html',
            ],
            'esc_js' => [
                'callable' => 'esc_js',
            ],
        ];

        /**
         * Filters the escaping filters that are added to Twig.
         *
         * The `$escaper_filters` array is an associative array with the filter name as a key and an
         * arguments array as the value. In the arguments array, you pass the function to call with
         * a `callable` entry.
         *
         *
         * @api
         * @since 2.1.0
         * @example
         * ```php
         * add_filter( 'timber/twig/escapers', function( $escaper_filters ) {
         *     // Add your own filter.
         *     $filters['esc_xml'] = [
         *         'callable' => 'esc_xml',
         *          'options' => [
         *             'is_safe' => ['html'],
         *          ],
         *     ];
         *
         *     // Remove a filter.
         *     unset( $filters['esc_js'] );
         *
         *     return $filters;
         * } );
         * ```
         *
         * @param array $escaper_filters
         */
        $escaper_filters = \apply_filters('timber/twig/escapers', $escaper_filters);

        return $escaper_filters;
    }

    /**
     * Adds filters to Twig.
     *
     * @param Environment $twig The Twig Environment.
     *
     * @return Environment
     */
    public function add_timber_filters($twig)
    {
        foreach ($this->get_timber_filters() as $name => $function) {
            $twig->addFilter(
                new TwigFilter(
                    $name,
                    $function['callable'],
                    $function['options'] ?? []
                )
            );
        }

        return $twig;
    }

    public function add_timber_escaper_filters($twig)
    {
        foreach ($this->get_timber_escaper_filters() as $name => $function) {
            $twig->addFilter(
                new TwigFilter(
                    $name,
                    $function['callable'],
                    $function['options'] ?? [
                        'is_safe' => ['html'],
                    ]
                )
            );
        }

        return $twig;
    }

    /**
     * Adds escapers.
     *
     * @param Environment $twig The Twig Environment.
     * @return Environment
     */
    public function add_timber_escapers($twig)
    {
        $esc_url = fn (Environment $env, $string) => \esc_url($string);

        $wp_kses_post = fn (Environment $env, $string) => \wp_kses_post($string);

        $esc_html = fn (Environment $env, $string) => \esc_html($string);

        $esc_js = fn (Environment $env, $string) => \esc_js($string);

        if (\class_exists(EscaperRuntime::class)) {
            $escaper_extension = $twig->getRuntime(EscaperRuntime::class);
            $escaper_extension->setEscaper('esc_url', '\esc_url');
            $escaper_extension->setEscaper('wp_kses_post', '\wp_kses_post');
            $escaper_extension->setEscaper('esc_html', '\esc_html');
            $escaper_extension->setEscaper('esc_js', '\esc_js');
        } elseif ($twig->hasExtension(EscaperExtension::class)) {
            $escaper_extension = $twig->getExtension(EscaperExtension::class);
            $escaper_extension->setEscaper('esc_url', $esc_url);
            $escaper_extension->setEscaper('wp_kses_post', $wp_kses_post);
            $escaper_extension->setEscaper('esc_html', $esc_html);
            $escaper_extension->setEscaper('esc_js', $esc_js);
        }

        return $twig;
    }

    /**
     * Overwrite Twig defaults.
     *
     * Makes Twig compatible with how WordPress handles dates, timezones, numbers and perhaps other items in
     * the future
     *
     * @since 2.0.0
     *
     * @throws \Twig\Error\RuntimeError
     * @param Environment $twig Twig Environment.
     *
     * @return Environment
     */
    public function set_defaults(Environment $twig)
    {
        $twig->getExtension(CoreExtension::class)->setDateFormat(\get_option('date_format'), '%d days');
        $twig->getExtension(CoreExtension::class)->setTimezone(\wp_timezone_string());

        /** @see https://developer.wordpress.org/reference/functions/number_format_i18n/ */
        global $wp_locale;
        if (isset($wp_locale)) {
            $twig->getExtension(CoreExtension::class)->setNumberFormat(0, $wp_locale->number_format['decimal_point'], $wp_locale->number_format['thousands_sep']);
        }

        return $twig;
    }

    /**
     * Converts a date to the given format.
     *
     * @internal
     * @since 2.0.0
     * @see  twig_date_format_filter()
     * @link https://twig.symfony.com/doc/2.x/filters/date.html
     *
     * @throws Exception
     *
     * @param Environment         $env      Twig Environment.
     * @param null|string|int|DateTime $date     A date.
     * @param null|string               $format   Optional. PHP date format. Will return the
     *                                            current date as a DateTimeImmutable object by
     *                                            default.
     * @param null                      $timezone Optional. The target timezone. Use `null` to use
     *                                            the default or
     *                                            `false` to leave the timezone unchanged.
     *
     * @return false|string A formatted date.
     */
    public function twig_date_format_filter(Environment $env, $date = null, $format = null, $timezone = null)
    {
        // Support for DateInterval.
        if ($date instanceof DateInterval) {
            if (null === $format) {
                $format = $env->getExtension(CoreExtension::class)->getDateFormat()[1];
            }

            return $date->format($format);
        }

        if (null === $date || 'now' === $date) {
            return DateTimeHelper::wp_date($format, null);
        }

        /**
         * If a string is given and it’s not a timestamp (e.g. "2010-01-28T15:00:00+04:00", try creating a DateTime
         * object and read the timezone from that string.
         */
        if (\is_string($date) && !\ctype_digit($date)) {
            $date_obj = \date_create($date);

            if ($date_obj) {
                $date = $date_obj;
            }
        }

        /**
         * Check for `false` parameter in |date filter in Twig
         *
         * @link https://twig.symfony.com/doc/2.x/filters/date.html#timezone
         */
        if (false === $timezone && $date instanceof DateTimeInterface) {
            $timezone = $date->getTimezone();
        }

        return DateTimeHelper::wp_date($format, $date, $timezone);
    }

    /**
     *
     *
     * @return array
     */
    public function to_array(mixed $arr)
    {
        if (\is_array($arr)) {
            return $arr;
        }
        $arr = [$arr];
        return $arr;
    }

    /**
     *
     *
     * @param string  $function_name
     * @return mixed
     */
    public function exec_function($function_name)
    {
        $args = \func_get_args();
        \array_shift($args);
        if (\is_string($function_name)) {
            $function_name = \trim($function_name);
        }
        return \call_user_func_array($function_name, ($args));
    }

    /**
     *
     *
     * @param string  $content
     * @return string
     */
    public function twig_pretags($content)
    {
        return \preg_replace_callback('|<pre.*>(.*)</pre|isU', [&$this, 'convert_pre_entities'], $content);
    }

    /**
     *
     *
     * @param array   $matches
     * @return string
     */
    public function convert_pre_entities($matches)
    {
        return \str_replace($matches[1], \htmlentities((string) $matches[1]), (string) $matches[0]);
    }

    /**
     * Formats a date.
     *
     * @deprecated 2.0.0
     *
     * @param null|string|false    $format Optional. PHP date format. Will use the `date_format`
     *                                     option as a default.
     * @param string|int|DateTime $date   A date.
     *
     * @return string
     */
    public function intl_date($date, $format = null)
    {
        Helper::deprecated('intl_date', 'DateTimeHelper::wp_date', '2.0.0');

        return DateTimeHelper::wp_date($format, $date);
    }

    /**
     *
     * @deprecated 2.0.0
     *
     * Returns the difference between two times in a human readable format.
     *
     * Differentiates between past and future dates.
     *
     * @see \human_time_diff()
     *
     * @param int|string $from          Base date as a timestamp or a date string.
     * @param int|string $to            Optional. Date to calculate difference to as a timestamp or
     *                                  a date string. Default to current time.
     * @param string     $format_past   Optional. String to use for past dates. To be used with
     *                                  `sprintf()`. Default `%s ago`.
     * @param string     $format_future Optional. String to use for future dates. To be used with
     *                                  `sprintf()`. Default `%s from now`.
     *
     * @return string
     */
    public static function time_ago($from, $to = null, $format_past = null, $format_future = null)
    {
        Helper::deprecated('time_ago', 'DateTimeHelper::time_ago', '2.0.0');

        return DateTimeHelper::time_ago($from, $to, $format_past, $format_future);
    }

    /**
     * @param array $arr
     * @param string $first_delimiter
     * @param string $second_delimiter
     * @return string
     */
    public function add_list_separators($arr, $first_delimiter = ',', $second_delimiter = ' and')
    {
        $length = \count($arr);
        $list = '';
        foreach ($arr as $index => $item) {
            if ($index < $length - 2) {
                $delimiter = $first_delimiter . ' ';
            } elseif ($index == $length - 2) {
                $delimiter = $second_delimiter . ' ';
            } else {
                $delimiter = '';
            }
            $list = $list . $item . $delimiter;
        }
        return $list;
    }
}
