<?php

/**
 *
 * @package   Duplicator
 * @copyright (c) 2022, Snap Creek LLC
 */

namespace Duplicator\Libs\Snap;

class SnapJson
{
    /**
     * Encode a variable into JSON, with some sanity checks.
     *
     * @since 4.1.0
     *
     * @param mixed $data    Variable (usually an array or object) to encode as JSON.
     * @param int   $options Optional. Options to be passed to json_encode(). Default 0.
     * @param int   $depth   Optional. Maximum depth to walk through $data. Must be
     *                       greater than 0. Default 512.
     *
     * @return string|false The JSON encoded string, or false if it cannot be encoded.
     */
    public static function jsonEncode($data, $options = 0, $depth = 512)
    {
        if (function_exists('wp_json_encode')) {
            return wp_json_encode($data, $options, $depth);
        }

        /*
         * json_encode() has had extra params added over the years.
         * $options was added in 5.3, and $depth in 5.5.
         * We need to make sure we call it with the correct arguments.
         */
        if (version_compare(PHP_VERSION, '5.5', '>=')) {
            $args = [
                $data,
                $options,
                $depth,
            ];
        } elseif (version_compare(PHP_VERSION, '5.3', '>=')) {
            $args = [
                $data,
                $options,
            ];
        } else {
            $args = [$data];
        }

        $preparedData = self::jsonPrepareData($data);
        // Prepare the data for JSON serialization.
        $args[0] = $preparedData;

        $json = @call_user_func_array('json_encode', $args);

        // If json_encode() was successful, no need to do more sanity checking.
        // ... unless we're in an old version of PHP, and json_encode() returned
        // a string containing 'null'. Then we need to do more sanity checking.
        if (false !== $json && ( version_compare(PHP_VERSION, '5.5', '>=') || false === strpos($json, 'null') )) {
            return $json;
        }

        try {
            $args[0] = self::jsonSanityCheck($preparedData, $depth);
        } catch (\Exception $e) {
            return false;
        }

        return call_user_func_array('json_encode', $args);
    }

    /**
     * json_encode with pretty print if define exists
     *
     * @param mixed $data    Variable (usually an array or object) to encode as JSON.
     * @param int   $options Optional. Options to be passed to json_encode(). Default 0.
     * @param int   $depth   Optional. Maximum depth to walk through $data. Must be
     *                       greater than 0. Default 512.
     *
     * @return string|false The JSON encoded string, or false if it cannot be encoded.
     */
    public static function jsonEncodePPrint($data, $options = 0, $depth = 512)
    {
        if (defined('JSON_PRETTY_PRINT')) {
            return self::jsonEncode($data, JSON_PRETTY_PRINT | $options, $depth);
        } else {
            return self::jsonEncode($data, $options, $depth);
        }
    }

    /**
     * Prepares response data to be serialized to JSON.
     *
     * This supports the JsonSerializable interface for PHP 5.2-5.3 as well.
     *
     * @param mixed $data Native representation.
     *
     * @return bool|int|float|null|string|mixed[] Data ready for `json_encode()`.
     */
    private static function jsonPrepareData($data)
    {
        if (
            !defined('WP_JSON_SERIALIZE_COMPATIBLE') ||
            WP_JSON_SERIALIZE_COMPATIBLE === false
        ) {
            return $data;
        }

        switch (gettype($data)) {
            case 'boolean':
            case 'integer':
            case 'double':
            case 'string':
            case 'NULL':
                // These values can be passed through.
                return $data;

            case 'array':
                // Arrays must be mapped in case they also return objects.
                return array_map([self::class, 'jsonPrepareData'], $data);

            case 'object':
                if ($data instanceof \__PHP_Incomplete_Class) {
                    return null;
                }

                $data = $data instanceof \JsonSerializable ? $data->jsonSerialize() : get_object_vars($data);

                // Now, pass the array (or whatever was returned from jsonSerialize through).
                return self::jsonPrepareData($data);

            default:
                return null;
        }
    }

    /**
     * Perform sanity checks on data that shall be encoded to JSON.
     *
     * @param mixed $data  Variable (usually an array or object) to encode as JSON.
     * @param int   $depth Maximum depth to walk through $data. Must be greater than 0.
     *
     * @return mixed The sanitized data that shall be encoded to JSON.
     */
    private static function jsonSanityCheck($data, $depth)
    {
        if ($depth < 0) {
            throw new \Exception('Reached depth limit');
        }

        if ($data instanceof \JsonSerializable) {
            $data = $data->jsonSerialize();
        }

        if (is_array($data)) {
            $output = [];
            foreach ($data as $id => $el) {
                // Don't forget to sanitize the ID!
                $clean_id = is_string($id) ? self::jsonConvertString($id) : $id;

                // Check the element type, so that we're only recursing if we really have to.
                if (is_array($el) || is_object($el)) {
                    $output[$clean_id] = self::jsonSanityCheck($el, $depth - 1);
                } elseif (is_string($el)) {
                    $output[$clean_id] = self::jsonConvertString($el);
                } else {
                    $output[$clean_id] = $el;
                }
            }
        } elseif (is_object($data)) {
            $output = new \stdClass();
            foreach ($data as $id => $el) {
                $clean_id = is_string($id) ? self::jsonConvertString($id) : $id;

                if (is_array($el) || is_object($el)) {
                    $output->$clean_id = self::jsonSanityCheck($el, $depth - 1);
                } elseif (is_string($el)) {
                    $output->$clean_id = self::jsonConvertString($el);
                } else {
                    $output->$clean_id = $el;
                }
            }
        } elseif (is_string($data)) {
            return self::jsonConvertString($data);
        } else {
            return $data;
        }

        return $output;
    }

    /**
     * Return json string
     *
     * @param string $string data
     *
     * @return string
     */
    private static function jsonConvertString(string $string)
    {
        static $use_mb = null;
        if (is_null($use_mb)) {
            $use_mb = function_exists('mb_convert_encoding');
        }

        if ($use_mb) {
            $encoding = mb_detect_encoding($string, mb_detect_order(), true);
            if ($encoding) {
                return mb_convert_encoding($string, 'UTF-8', $encoding);
            } else {
                return mb_convert_encoding($string, 'UTF-8', 'UTF-8');
            }
        } else {
            return self::checkInvalidUTF8($string, true);
        }
    }

    /**
     * Checks for invalid UTF8 in a string.
     *
     * @param string $string The text which is to be checked.
     * @param bool   $strip  Optional. Whether to attempt to strip out invalid UTF8. Default is false.
     *
     * @return string The checked text.
     */
    public static function checkInvalidUTF8($string, $strip = false)
    {
        $string = (string) $string;

        if (0 === strlen($string)) {
            return '';
        }

        // Check for support for utf8 in the installed PCRE library once and store the result in a static
        static $utf8_pcre = null;
        if (!isset($utf8_pcre)) {
            $utf8_pcre = @preg_match('/^./u', 'a');
        }
        // We can't demand utf8 in the PCRE installation, so just return the string in those cases
        if (!$utf8_pcre) {
            return $string;
        }

        // preg_match fails when it encounters invalid UTF8 in $string
        if (1 === @preg_match('/^./us', $string)) {
            return $string;
        }

        // Attempt to strip the bad chars if requested (not recommended)
        if ($strip && function_exists('iconv')) {
            return iconv('utf-8', 'utf-8', $string);
        }

        return '';
    }

    /**
     * todo remove esc_attr wp function
     *
     * @param mixed $val object to be encoded
     *
     * @return string escaped json string
     */
    public static function jsonEncodeEscAttr($val)
    {
        return esc_attr(json_encode($val));
    }

    /**
     * this function return a json encoded string without quotes at the beginning and the end
     *
     * @param string $string json string
     *
     * @return string
     */
    public static function getJsonWithoutQuotes($string): string
    {
        if (!is_string($string)) {
            throw new \Exception('the function getJsonStringWithoutQuotes take only strings');
        }

        return substr(self::jsonEncode($string), 1, -1);
    }
}
