<?php
/*
 * This class automatically inserts HTML tags to make text clean and pretty.
 * With this class you can choose to execute any of the functions independently, for example,
 * if you only want lists, or just BBCode. Note: doParagraphs should be run after doLists.
 * 
 * Usage:
 * 
 * $richText = new RichText();
 * //Note: important to sanitize *before* adding rich text.
 * $string = htmlentities($string, ENT_QUOTE, 'utf-8'); 
 * $string = $richText->doBBCode(
 *     $richText->doParagraphs(
 *         $richText->doLists($string)
 *     ),
 *     array('halls-of-valhalla.org')
 * );
 *
 */
namespace Valhalla\BaseBundle\Classes;

class RichText {
    CONST UL_PATTERN = '[*+-]';
    CONST OL_PATTERN = '[0-9]+[.]';

    /**
     * Block-level HTML elements which should not be placed in <p> tags
     *
     * @var array
     */
    public static $blockHTMLElements = array(
        'div', 'section', 'article',
        'table',
        'iframe',
        'code',
        'pre',
        'p',
        'ul', 'ol',
        'h1', 'h2', 'h3', 'h4', 'h5',
    );

    /**
     * Block-level BBCode elements which should not be placed in <p> tags
     *
     * @var array
     */
    public static $blockBBCodeElements = array(
        'code',
        'quote',
        'video',
        'h',
        'center',
    );

    /**
     * Replaces BBCode with HTML.
     *
     * @param string $string
     * @param array $domainWhitelist URLs to not mark as nofollow
     * @return string
     */
    public function doBBCode($string, $domainWhitelist = array()) {
        $search = array(
            '@\[b\](.*?)\[/b\]@si',
            '@\[i\](.*?)\[/i\]@si',
            '@\[u\](.*?)\[/u\]@si',
            '@\[h\](.*?)\[/h\]@si',
            '@\[img\](?:http(s)?://)?(.*?)\[/img\]@si',
            '@\[url=(?:http(s)?://)?(.*?)\](.*?)\[/url\]@si',
            '@\[code\](.*?)\[/code\]@si',
            '@\[video\]http://www\.youtube\.com/watch\?v=(.*?)(&.*)*\[/video\]@si',
            '@\[highlight\](.*?)\[/highlight]@si',
            '@\[center\](.*?)\[/center]@si',
            '@\[quote\](.*?)\[/quote]@si',
            '@\[sup\](.*?)\[/sup]@si',
        );
        $replace = array(
            '<b>\\1</b>',
            '<i>\\1</i>',
            '<u>\\1</u>',
            '<h4>\\1</h4>',
            '<img src="http\\1://\\2" alt="\\2" class="imgTag"/>',
            '<a href="http\\1://\\2">\\3</a>',
            '<pre class="codeTag prettyprint">\\1</pre>',
            '<iframe width="560" height="315" src="http://www.youtube.com/embed/\\1" frameborder="0" allowfullscreen></iframe>',
            '<font color="red">\\1</font>',
            '<div align="center">\\1</div>',
            '<pre class="quote">\\1</pre>',
            '<sup>\\1</sup>',
        );
        $currentHTML = preg_replace($search , $replace, $string);

        $currentHTML = preg_replace(
            '%(<a\s*(?!.*\brel=)[^>]*)(href="https?://)((?!(?:(?:www\.)?'
                . implode('|(?:www\.)?', $domainWhitelist) .
                '))[^"]+)"((?!.*\brel=)[^>]*)(?:[^>]*)>%',
            '$1$2$3"$4 rel="nofollow">',
            $currentHTML);

        return $currentHTML;
    }


    /**
     * Automatically turns lists beginning with hyphens, asterisks, or numbers into ul/ol tags.
     *
     * @param string $text
     * @return string
     */
    public function doLists($text){
        $finalLines = array();

        $text = $this->prepareText($text);
        $lines = explode("\n", $text);

        $listLines = array();
        $openBlockElements = array();
        foreach($lines as $line){
            list($listType, $pattern) = (empty($line) or $line[0] <= '-') ? array('ul', self::UL_PATTERN) : array('ol', self::OL_PATTERN);
            $openBlockElements = array_filter($this->checkForOpenBlockElements($line, $openBlockElements));
            $blockElementsContained = !empty($openBlockElements);

            if($blockElementsContained){
                $finalLines = $this->writeListLines($listLines, $finalLines, true);
                $finalLines[] = $line;
                $listLines = array();
            } elseif (preg_match('/^('.$pattern.'[ ]+)(.*)/', $line, $matches)) {
                if(empty($listLines) or $listLines[0]['listType'] == $listType){
                    $listLines[] = array(
                        'listType' => $listType,
                        'content' => $matches[2],
                        'rawContent' => $line,
                    );
                } else { //Not a valid list; abort
                    $finalLines = $this->writeListLines($listLines, $finalLines, true);
                    $finalLines[] = $line;
                    $listLines = array();
                }
            } else { //Normal line
                $finalLines = $this->writeListLines($listLines, $finalLines);
                $listLines = array();
                $finalLines[] = $line;
            }

            $openBlockElements = $this->checkForClosingBlockElements($line, $openBlockElements);
        }
        $finalLines = $this->writeListLines($listLines, $finalLines);

        return implode("\n", $finalLines);
    }

    /**
     * Adds the list to the fine array of lines. If $raw=true then just add the plain text to the array.
     *
     * @param array $listLines
     * @param array $finalLines
     * @param boolean $raw
     * @return array
     */
    protected function writeListLines(array $listLines, array $finalLines, $raw = false){
        if(!empty($listLines)){
            if($raw or count($listLines) == 1){
                foreach($listLines as $listLine){
                    $finalLines[] = $listLine['rawContent'];
                }
            } else {
                $finalLines[] = "<{$listLines[0]['listType']} class='bbcodeList'>";
                foreach($listLines as $listLine){
                    $finalLines[] = "<li>{$listLine['content']}</li>";
                }
                $finalLines[] = "</{$listLines[0]['listType']}>";
            }
        }

        return $finalLines;
    }

    /**
     * Automatically places p tags around paragraphs.
     *
     * @param string $text
     * @return string
     */
    public function doParagraphs($text){
        $finalText = array();

        $text = $this->prepareBlockElements($text);
        $paragraphs = explode("\n\n", $text);

        $openBlockElements = array();
        foreach($paragraphs as $paragraph){
            $openBlockElements = array_filter($this->checkForOpenBlockElements($paragraph, $openBlockElements));
            $blockElementsContained = !empty($openBlockElements);

            //Don't put any p tags around block elements.
            if($blockElementsContained) {
                $finalText[] = $paragraph;
            } else {
                $finalText[] = '<p>' . nl2br($paragraph) . '</p>';
            }

            $openBlockElements = $this->checkForClosingBlockElements($paragraph, $openBlockElements);
        }

        return implode("\n", $finalText);
    }

    /**
     * Checks if the given text contains any block elements. And returns a tally of open elements.
     *
     * @param string $snippet
     * @param array $openBlockElements
     * @return array
     */
    public function checkForOpenBlockElements($snippet, $openBlockElements = array()){
        foreach(self::$blockHTMLElements as $element){
            if(preg_match("/\\<{$element}(\\s.*)?\\>/", $snippet)){
                $openBlockElements[$element] = (isset($openBlockElements[$element])) ? $openBlockElements[$element] + 1 : 1;
            }
        }

        foreach(self::$blockBBCodeElements as $element){
            if(preg_match("/\\[{$element}]/", $snippet)){
                $openBlockElements[$element] = (isset($openBlockElements[$element])) ? $openBlockElements[$element] + 1 : 1;
            }
        }

        return $openBlockElements;
    }

    /**
     * Checks if the given text contains any closing blcok elements and returns a tally of open elements
     *
     * @param string $snippet
     * @param array $openBlockElements
     * @return array
     */
    public function checkForClosingBlockElements($snippet, $openBlockElements = array()){
        foreach(self::$blockHTMLElements as $element){
            if(preg_match("/\\<\\/{$element}>/", $snippet)){
                $openBlockElements[$element] = (isset($openBlockElements[$element])) ? $openBlockElements[$element] - 1 : 0;
            }
        }

        foreach(self::$blockBBCodeElements as $element){
            if(preg_match("/\\[\\/{$element}]/", $snippet)){
                $openBlockElements[$element] = (isset($openBlockElements[$element])) ? $openBlockElements[$element] - 1 : 0;
            }
        }

        return $openBlockElements;
    }

    /**
     * Inserts line breaks before and after all block elements.
     *
     * @param string $text
     * @return string
     */
    public function prepareBlockElements($text){
        foreach(self::$blockHTMLElements as $element){
            $text = preg_replace("/\\<{$element}(\\s.*)?\\>/", "\n\n\\0", $text);
            $text = preg_replace("/\\<\\/{$element}>/", "\\0\n\n", $text);
        }

        foreach(self::$blockBBCodeElements as $element){
            $text = preg_replace("/\\[{$element}]/", "\n\n\\0", $text);
            $text = preg_replace("/\\[\\/{$element}]/", "\\0\n\n", $text);
        }

        $text = preg_replace("/\n{2,}/", "\n\n", $text); //no more than two in a row

        return $text;
    }

    /**
     * Prepares and formats text for modification.
     *
     * @param string $text
     * @return string
     */
    public function prepareText($text){
        $text = str_replace("\r\n", "\n", $text);
        $text = str_replace("\r", "\n", $text);
        # replace tabs with spaces
        $text = str_replace("\t", '    ', $text);
        # remove surrounding line breaks
        $text = trim($text, "\n");

        return $text;
    }

}