<?php
/**
 * Functions useful for working with text.
 *
 * \@package    Helpers
 *
 * @category   Concrete
 *
 * @author     Andrew Embler <andrew@concrete5.org>
 * @copyright  Copyright (c) 2003-2008 Concrete5. (http://www.concrete5.org)
 * @license    http://www.concrete5.org/license/     MIT License
 */
namespace Concrete\Core\Utility\Service;

use Concrete\Core\Config\Repository\Repository;
use Concrete\Core\Foundation\ConcreteObject;
use Config;
use Concrete\Core\Support\Facade\Application;
use DOMDocument;
use Patchwork\Utf8;

class Text
{
    /**
     * URL-encodes collection path.
     *
     * @param string|null $path
     *
     * @return string $path
     */
    public static function encodePath($path)
    {
        $path = (string) $path;
        if (mb_strpos($path, '/') !== false) {
            $path = explode('/', $path);
            $path = array_map('rawurlencode', $path);
            $newPath = implode('/', $path);
        } else {
            $newPath = rawurlencode($path);
        }
        $path = str_replace('%21', '!', $newPath);

        return $path;
    }

    /**
     * Determine if a given string matches a given pattern.
     *
     * @param  string $pattern
     * @param  string $value
     *
     * @return bool
     */
    public static function match($pattern, $value)
    {
        if ($pattern == $value) {
            return true;
        }

        $pattern = preg_quote($pattern, '#');
        if ($pattern !== '/') {
            $pattern = str_replace('\*', '.*', $pattern) . '\z';
        } else {
            $pattern = '/$';
        }

        return (bool) preg_match('#^' . $pattern . '#', $value);
    }

    /**
     * Remove unsafe characters for URL slug.
     *
     * @param string $handle
     * @param int    $maxlength = Max number of characters of the return value
     *
     * @return string $handle
     */
    public function slugSafeString($handle, $maxlength = 128)
    {
        $handle = preg_replace('/[^\\p{L}\\p{Nd}\-_]+/u', ' ', $handle); // remove unneeded chars
        $handle = preg_replace('/[-\s]+/', '-', $handle); // convert spaces to hyphens
        return trim(Utf8::substr($handle, 0, $maxlength), '-'); // trim to first $max_length chars
    }

    /**
     * Strips tags and optionally reduces string to specified length.
     *
     * @param string $string
     * @param int    $max_length
     * @param string $allowed
     *
     * @return string
     */
    public function sanitize($string, $max_length = 0, $allowed = '')
    {
        $text = trim(strip_tags($string, $allowed));
        if ($max_length > 0) {
            if (function_exists('mb_substr')) {
                $text = mb_substr($text, 0, $max_length, APP_CHARSET);
            } else {
                $text = substr($text, 0, $max_length);
            }
        }
        if ($text == null) {
            return ""; // we need to explicitly return a string otherwise some DB functions might insert this as a ZERO.
        }

        return $text;
    }

    /**
     * Leaves only characters that are valid in email addresses (RFC).
     *
     * @param string $email
     *
     * @return string
     */
    public function email($email)
    {
        $regex = '/[^a-zA-Z0-9_\.!#\$\&\'\*\+-?^`{|}~@]/i';

        return preg_replace($regex, '', $email);
    }

    /**
     * Leaves only characters that are alpha-numeric.
     *
     * @param string $string
     *
     * @return string
     */
    public function alphanum($string)
    {
        return preg_replace('/[^A-Za-z0-9]/', '', $string);
    }

    /**
     * always use in place of htmlentites(), so it works with different languages.
     *
     * @param string $v
     *
     * @return string
     */
    public function entities($v)
    {
        return htmlentities($v, ENT_QUOTES, APP_CHARSET);
    }

    /**
     * Decodes html-encoded entities (for instance: from '&gt;' to '>').
     *
     * @param string $v
     *
     * @return string
     */
    public function decodeEntities($v)
    {
        return html_entity_decode($v, ENT_QUOTES, APP_CHARSET);
    }

    /**
     * A concrete5 specific version of htmlspecialchars(). Double encoding is OFF, and the character set is set to your site's.
     *
     * @param string $v
     *
     * @return string
     */
    public function specialchars($v)
    {
        return htmlspecialchars((string) $v, ENT_QUOTES, APP_CHARSET, false);
    }

    /**
     * An alias for shortText().
     *
     * @param string $textStr
     * @param int    $numChars
     * @param string $tail
     *
     * @return string
     */
    public function shorten($textStr, $numChars = 255, $tail = '…')
    {
        return $this->shortText($textStr, $numChars, $tail);
    }

    /**
     * Like sanitize, but requiring a certain number characters, and assuming a tail.
     *
     * @param string $textStr
     * @param int    $numChars
     * @param string $tail
     *
     * @return string $textStr
     */
    public function shortText($textStr, $numChars = 255, $tail = '…')
    {
        if (intval($numChars) == 0) {
            $numChars = 255;
        }
        $textStr = strip_tags($textStr);
        if (function_exists('mb_substr') && function_exists('mb_strlen')) {
            if (mb_strlen($textStr, APP_CHARSET) > $numChars) {
                $textStr = mb_substr($textStr, 0, $numChars, APP_CHARSET) . $tail;
            }
        } else {
            if (strlen($textStr) > $numChars) {
                $textStr = substr($textStr, 0, $numChars) . $tail;
            }
        }

        return $textStr;
    }

    /**
     * Takes a string and turns it into the CamelCase or StudlyCaps version.
     *
     * @param string $string
     *
     * @return string
     */
    public function camelcase($string)
    {
        return ConcreteObject::camelcase($string);
    }

    /**
     * automatically add hyperlinks to any twitter style @usernames in a string.
     *
     * @param string $input
     * @param int    $newWindow
     * @param int    $withSearch
     *
     * @return string $output
     */
    public function twitterAutolink($input, $newWindow = 0, $withSearch = 0)
    {
        $target = ($newWindow) ? ' target="_blank" ' : '';
        $output = preg_replace(
            '/([\.|\,|\:|\¡|\¿|\>|\{|\(]?)@{1}(\w*)([\.|\,|\:|\!|\?|\>|\}|\)]?)\s/i',
            "$1<a href=\"https://twitter.com/$2\" " . $target . " class=\"twitter-username\">@$2</a>$3 ",
            $input);
        if ($withSearch) {
            $output = preg_replace(
                '/([\.|\,|\:|\¡|\¿|\>|\{|\(]?)#{1}(\w*)([\.|\,|\:|\!|\?|\>|\}|\)]?)\s/i',
                "$1<a href=\"https://twitter.com/search?q=%23$2\" " . $target . " class=\"twitter-search\">#$2</a>$3 ",
                $input);
        }

        return $output;
    }

    /**
     * Runs a number of text functions, including autolink, nl2br, strip_tags. Assumes that you want simple
     * text comments but with a few niceties.
     *
     * @param string $input
     *
     * @return string $output
     */
    public function makenice($input)
    {
        $output = strip_tags($input);
        $output = $this->autolink($output);
        $output = nl2br($output);

        return $output;
    }

    /**
     * Runs strip_tags but ensures that spaces are kept between the stripped tags.
     *
     * @param $input
     * @param $allowedTags
     */
    public function prettyStripTags($input, $allowedTags = null)
    {
        return str_replace('  ', ' ', strip_tags(str_replace('<', ' <', $input), $allowedTags));
    }

    /**
     * Scans passed text and automatically hyperlinks any URL inside it.
     *
     * @param string $input
     * @param bool $newWindow
     * @param string $defaultProtocol
     *
     * @return string $output
     */
    public function autolink($input, $newWindow = false, $defaultProtocol = 'http://')
    {
        $target = $newWindow ? ' target="_blank"' : '';
        $output = preg_replace_callback(
            '/(http:\/\/|https:\/\/|(www\.))(([^\s<]{4,80})[^\s<]*)/',
            function (array $matches) use ($target, $defaultProtocol) {
                $protocol = strpos($matches[1], ':') ? $matches[1] : $defaultProtocol;
                return "<a href=\"{$protocol}{$matches[2]}{$matches[3]}\"{$target} rel=\"nofollow\">{$matches[2]}{$matches[4]}</a>";
            },
            $input);

        return $output;
    }

    /**
     * A wrapper for PHP's fnmatch() function, which some installations don't have.
     *
     * @param string $pattern
     * @param string $string
     *
     * @return bool
     */
    public function fnmatch($pattern, $string)
    {
        $string = (string) $string;
        if (!function_exists('fnmatch')) {
            return preg_match(
                "#^" . strtr(
                    preg_quote($pattern, '#'),
                    array('\*' => '.*', '\?' => '.', '\[' => '[', '\]' => ']')) . "$#i",
                $string);
        } else {
            return fnmatch($pattern, $string);
        }
    }

    /**
     * Takes a CamelCase string and turns it into camel_case.
     * See also laravel function snake_case which does similar but without bugs.
     *
     * @param string $string
     *
     * @return string
     */
    public function uncamelcase($string)
    {
        return ConcreteObject::uncamelcase($string);
    }

    /**
     * Takes a handle-based string like "blah_blah" or "blah-blah" or "blah/blah" and turns it into "Blah Blah".
     *
     * @param string $string
     *
     * @return string $r1
     */
    public function unhandle($string)
    {
        $r1 = ucwords(str_replace(array('_', '-', '/'), ' ', $string));

        return $r1;
    }

    /**
     * Takes a string and turns it into a handle.
     *
     * @param      $handle
     * @param bool $leaveSlashes
     *
     * @return string
     */
    public function handle($handle, $leaveSlashes = false)
    {
        $handle = $this->sanitizeFileSystem($handle, $leaveSlashes);
        return str_replace('-', '_', $handle);
    }

    /**
     * Determines whether a string matches a particular pattern.
     *
     * @author Laravel Framework
     */

    /**
     * @param string $handle
     *
     * @return string $handle
     */
    public function sanitizeFileSystem($handle)
    {
        return $this->urlify($handle, Config::get('concrete.seo.segment_max_length'), '', false);
    }

    /**
     * Takes text and returns it in the "lowercase-and-dashed-with-no-punctuation" format.
     *
     * @param string $handle
     * @param int    $max_length           Max number of characters of the return value
     * @param string $locale              Language code of the language rules that should be priorized
     * @param bool   $removeExcludedWords Set to true to remove excluded words, false to allow them.
     *
     * @return string
     */
    public function urlify($handle, $max_length = null, $locale = '', $removeExcludedWords = true)
    {
        $app = Application::getFacadeApplication();
        $config = $app->make(Repository::class);
        if ($max_length === null) {
            $max_length = $config->get('concrete.seo.segment_max_length');
        }
        if ($config->get('concrete.seo.enable_slug_asciify')) {
            $text = $this->asciify($handle, $locale);
        } else {
            $text = $handle;
        }
        $text = strtolower(str_replace(array("\r", "\n", "\t"), ' ', $text));
        if ($removeExcludedWords) {
            $excludeSeoWords = $config->get('concrete.seo.exclude_words');
            if (is_string($excludeSeoWords)) {
                if (strlen($excludeSeoWords)) {
                    $remove_list = explode(',', $excludeSeoWords);
                    $remove_list = array_map('trim', $remove_list);
                    $remove_list = array_filter($remove_list, 'strlen');
                } else {
                    $remove_list = array();
                }
            } else {
                $remove_list = \URLify::$remove_list;
            }
            if (count($remove_list)) {
                $text = preg_replace('/\b(' . implode('|', $remove_list) . ')\b/i', '', $text);
            }
        }
        $text = preg_replace('/[^-\w\s]/', '', $text);   // remove unneeded chars
        $text = str_replace('_', ' ', $text);            // treat underscores as spaces
        $text = preg_replace('/^\s+|\s+$/', '', $text);  // trim leading/trailing spaces
        $text = preg_replace('/[-\s]+/', '-', $text);    // convert spaces to hyphens
        $text = strtolower($text);                       // convert to lowercase
        return trim(substr($text, 0, $max_length), '-'); // trim to first $maxlength chars
    }

    /**
     * Takes text and converts it to an ASCII-only string (characters with code between 32 and 127, plus \t, \n and \r).
     *
     * @param string $text   The text to be converted.
     * @param string $locale ='' The locale for the string. If not specified we consider the current locale.
     *
     * @return string
     */
    public function asciify($text, $locale = '')
    {
        if (!strlen($locale)) {
            $locale = \Localization::activeLocale();
        }
        $language = substr($locale, 0, strcspn($locale, '_'));
        $text = \URLify::downcode($text, $language);
        if (preg_match('/[^\\t\\r\\n\\x20-\\x7e]/', $text)) {
            if (function_exists('iconv')) {
                $t = @iconv(APP_CHARSET, 'US-ASCII//IGNORE//TRANSLIT', $text);
                if (is_string($t)) {
                    $text = $t;
                }
            }
            $text = preg_replace('/[^\\t\\r\\n\\x20-\\x7e]/', '', $text);
        }

        return $text;
    }

    /**
     * alias of shortenTextWord().
     *
     * @param string $textStr
     * @param int    $numChars
     * @param string $tail
     *
     * @return string
     */
    public function wordSafeShortText($textStr, $numChars = 255, $tail = '…')
    {
        return $this->shortenTextWord($textStr, $numChars, $tail);
    }

    /**
     * Shortens and sanitizes a string but only cuts at word boundaries.
     *
     * @param        $textStr
     * @param int    $numChars
     * @param string $tail
     *
     * @return string
     */
    public function shortenTextWord($textStr, $numChars = 255, $tail = '…')
    {
        if (intval($numChars) == 0) {
            $numChars = 255;
        }
        $textStr = strip_tags($textStr);
        if (function_exists('mb_substr')) {
            if (mb_strlen($textStr, APP_CHARSET) > $numChars) {
                $textStr = preg_replace('/\s+?(\S+)?$/', '', mb_substr($textStr, 0, $numChars + 1, APP_CHARSET));
                // this is needed if the shortened string consists of one single word
                $textStr = mb_substr($textStr, 0, $numChars, APP_CHARSET) . $tail;
            }
        } else {
            if (strlen($textStr) > $numChars) {
                $textStr = preg_replace('/\s+?(\S+)?$/', '', substr($textStr, 0, $numChars + 1));
                $textStr = substr($textStr, 0, $numChars) . $tail;
            }
        }

        return $textStr;
    }

    /**
     * Strips out non-alpha-numeric characters.
     *
     * @param string $val
     *
     * @return string
     */
    public function filterNonAlphaNum($val)
    {
        return preg_replace('/[^[:alnum:]]/', '', $val);
    }

    /**
     * Highlights a string within a string with the class ccm-highlight-search.
     *
     * @param string $value
     * @param string $searchString
     *
     * @return string
     */
    public function highlightSearch($value, $searchString)
    {
        if (strlen($value) < 1 || strlen($searchString) < 1) {
            return $value;
        }
        preg_match_all("/$searchString+/i", $value, $matches);
        if (is_array($matches[0]) && count($matches[0]) > 0) {
            return str_replace($matches[0][0], '<em class="ccm-highlight-search">' . $matches[0][0] . '</em>', $value);
        }

        return $value;
    }

    /**
     * Formats a passed XML string nicely.
     *
     * @param $xml
     *
     * @return string
     */
    public function formatXML($xml)
    {
        $dom = new DOMDocument();
        $dom->preserveWhiteSpace = false;
        $dom->loadXML($xml);
        $dom->formatOutput = true;

        return $dom->saveXml();
    }

    /**
     * Appends a SimpleXMLElement to a SimpleXMLElement.
     *
     * @param \SimpleXMLElement $root
     * @param \SimpleXMLElement $new
     */
    public function appendXML(\SimpleXMLElement $root, \SimpleXMLElement $new)
    {
        $node = $root->addChild($new->getName(), (string) $new);
        foreach ($new->attributes() as $attr => $value) {
            $node->addAttribute($attr, $value);
        }
        foreach ($new->children() as $ch) {
            $this->appendXML($node, $ch);
        }
    }
}
