<?php

/**
 * @author     Nick Pope <nick@nickpope.me.uk>
 * @copyright  Copyright © 2010, Nick Pope
 * @license    http://www.apache.org/licenses/LICENSE-2.0  Apache License v2.0
 */

namespace App\Util\Lexer;

/**
 * Twitter HitHighlighter Class.
 *
 * Performs "hit highlighting" on tweets that have been auto-linked already.
 * Useful with the results returned from the search API.
 *
 * Originally written by {@link http://github.com/mikenz Mike Cochrane}, this
 * is based on code by {@link http://github.com/mzsanford Matt Sanford} and
 * heavily modified by {@link http://github.com/ngnpope Nick Pope}.
 *
 * @author     Nick Pope <nick@nickpope.me.uk>
 * @copyright  Copyright © 2010, Nick Pope
 * @license    http://www.apache.org/licenses/LICENSE-2.0  Apache License v2.0
 */
class HitHighlighter extends Regex
{
    /**
     * The tag to surround hits with.
     *
     * @var string
     */
    protected $tag = 'em';

    /**
     * Provides fluent method chaining.
     *
     * @param  string  $tweet  The tweet to be hit highlighted.
     * @param  bool  $full_encode  Whether to encode all special characters.
     *
     * @see  __construct()
     *
     * @return HitHighlighter
     */
    public static function create($tweet = null, $full_encode = false)
    {
        return new self($tweet, $full_encode);
    }

    /**
     * Reads in a tweet to be parsed and hit highlighted.
     *
     * We take this opportunity to ensure that we escape user input.
     *
     * @see  htmlspecialchars()
     *
     * @param  string  $tweet  The tweet to be hit highlighted.
     * @param  bool  $escape  Whether to escape the tweet (default: true).
     * @param  bool  $full_encode  Whether to encode all special characters.
     */
    public function __construct($tweet = null, $escape = true, $full_encode = false)
    {
        if (! empty($tweet) && $escape) {
            if ($full_encode) {
                parent::__construct(htmlentities($tweet, ENT_QUOTES, 'UTF-8', false));
            } else {
                parent::__construct(htmlspecialchars($tweet, ENT_QUOTES, 'UTF-8', false));
            }
        } else {
            parent::__construct($tweet);
        }
    }

    /**
     * Set the highlighting tag to surround hits with.  The default tag is 'em'.
     *
     * @return string The tag name.
     */
    public function getTag()
    {
        return $this->tag;
    }

    /**
     * Set the highlighting tag to surround hits with.  The default tag is 'em'.
     *
     * @param  string  $v  The tag name.
     * @return HitHighlighter Fluid method chaining.
     */
    public function setTag($v)
    {
        $this->tag = $v;

        return $this;
    }

    /**
     * Hit highlights the tweet.
     *
     * @param  string  $tweet  The tweet to be hit highlighted.
     * @param  array  $hits  An array containing the start and end index pairs
     *                       for the highlighting.
     * @param  bool  $escape  Whether to escape the tweet (default: true).
     * @param  bool  $full_encode  Whether to encode all special characters.
     * @return string The hit highlighted tweet.
     */
    public function highlight($tweet = null, ?array $hits = null)
    {
        if (is_null($tweet)) {
            $tweet = $this->tweet;
        }
        if (empty($hits)) {
            return $tweet;
        }
        $highlightTweet = '';
        $tags = ['<'.$this->tag.'>', '</'.$this->tag.'>'];
        // Check whether we can simply replace or whether we need to chunk...
        if (strpos($tweet, '<') === false) {
            $ti = 0; // tag increment (for added tags)
            $highlightTweet = $tweet;
            foreach ($hits as $hit) {
                $highlightTweet = StringUtils::substrReplace($highlightTweet, $tags[0], $hit[0] + $ti, 0);
                $ti += StringUtils::strlen($tags[0]);
                $highlightTweet = StringUtils::substrReplace($highlightTweet, $tags[1], $hit[1] + $ti, 0);
                $ti += StringUtils::strlen($tags[1]);
            }
        } else {
            $chunks = preg_split('/[<>]/iu', $tweet);
            $chunk = $chunks[0];
            $chunk_index = 0;
            $chunk_cursor = 0;
            $offset = 0;
            $start_in_chunk = false;
            // Flatten the multidimensional hits array:
            $hits_flat = [];
            foreach ($hits as $hit) {
                $hits_flat = array_merge($hits_flat, $hit);
            }
            // Loop over the hit indices:
            for ($index = 0; $index < count($hits_flat); $index++) {
                $hit = $hits_flat[$index];
                $tag = $tags[$index % 2];
                $placed = false;
                while ($chunk !== null && $hit >= ($i = $offset + StringUtils::strlen($chunk))) {
                    $highlightTweet .= StringUtils::substr($chunk, $chunk_cursor);
                    if ($start_in_chunk && $hit === $i) {
                        $highlightTweet .= $tag;
                        $placed = true;
                    }
                    if (isset($chunks[$chunk_index + 1])) {
                        $highlightTweet .= '<'.$chunks[$chunk_index + 1].'>';
                    }
                    $offset += StringUtils::strlen($chunk);
                    $chunk_cursor = 0;
                    $chunk_index += 2;
                    $chunk = (isset($chunks[$chunk_index]) ? $chunks[$chunk_index] : null);
                    $start_in_chunk = false;
                }
                if (! $placed && $chunk !== null) {
                    $hit_spot = $hit - $offset;
                    $highlightTweet .= StringUtils::substr($chunk, $chunk_cursor, $hit_spot - $chunk_cursor).$tag;
                    $chunk_cursor = $hit_spot;
                    $start_in_chunk = ($index % 2 === 0);
                    $placed = true;
                }
                // Ultimate fallback - hits that run off the end get a closing tag:
                if (! $placed) {
                    $highlightTweet .= $tag;
                }
            }
            if ($chunk !== null) {
                if ($chunk_cursor < StringUtils::strlen($chunk)) {
                    $highlightTweet .= StringUtils::substr($chunk, $chunk_cursor);
                }
                for ($index = $chunk_index + 1; $index < count($chunks); $index++) {
                    $highlightTweet .= ($index % 2 === 0 ? $chunks[$index] : '<'.$chunks[$index].'>');
                }
            }
        }

        return $highlightTweet;
    }

    /**
     * Hit highlights the tweet.
     *
     * @param  array  $hits  An array containing the start and end index pairs
     *                       for the highlighting.
     * @return string The hit highlighted tweet.
     *
     * @deprecated since version 1.1.0
     */
    public function addHitHighlighting(array $hits)
    {
        return $this->highlight($this->tweet, $hits);
    }
}
