<?php

namespace Mnv\Core\Utilities\BBCode;

use Closure;
use Mnv\Core\Config;

/**
 * Class BBCode
 * BBCode to HTML converter
 * @package Mnv\Core\Utilities\BBCode
 */
class BBCode
{

    /**
     * Константы с именами встроенных тегов по умолчанию
     * Вызовите getDefaultTagNames(), чтобы получить их в виде массива.
     */
    const TAG_NAME_B         = 'b';
    const TAG_NAME_I         = 'i';
    const TAG_NAME_S         = 's';
    const TAG_NAME_U         = 'u';
    const TAG_NAME_CODE      = 'code';
    const TAG_NAME_EMAIL     = 'email';
    const TAG_NAME_URL       = 'url';
    const TAG_NAME_IMG       = 'img';
    const TAG_NAME_LIST      = 'list';
    const TAG_NAME_LI_STAR   = '*';
    const TAG_NAME_LI        = 'li';
    const TAG_NAME_QUOTE     = 'quote';
    const TAG_NAME_YOUTUBE   = 'youtube';
    const TAG_NAME_VIDEO     = 'video';
    const TAG_NAME_FONT      = 'font';
    const TAG_NAME_SIZE      = 'size';
    const TAG_NAME_COLOR     = 'color';
    const TAG_NAME_LEFT      = 'left';
    const TAG_NAME_CENTER    = 'center';
    const TAG_NAME_RIGHT     = 'right';
    const TAG_NAME_SPOILER   = 'spoiler';


    /**
     * Текст с BBCodes
     *
     * @var string|null
     */
    protected $text = null;

    /**
     * Массив с настраиваемым тегом Closures
     *
     * @var Closure[]
     */
    protected $customTagClosures = array();

    /**
     * Массив (имя) тегов, которые игнорируются
     *
     * @var string[]
     */
    protected $ignoredTags = array();

    /**
     * Ширина (в пикселях) элемента iframe YouTube
     *
     * @var int
     */
    protected $youTubeWidth = 640;

    /**
     * Высота (в пикселях) элемента iframe YouTube
     *
     * @var int
     */
    protected $youTubeHeight = 385;

    /**
     * @var array
     */
    protected $video_config;

    /**
     * BBCode constructor.
     *
     * @param string|null $text Текст — может включать теги BBCode.
     */
    public function __construct($text = null)
    {
        $this->setText($text);

        $this->video_config = GLOBAL_ROOT."/admin/config/videoconfig.php";
    }

    /**
     * Установите необработанный текст - может включать теги BBCode
     *
     * @param string $text The text
     * @retun void
     */
    public function setText($text)
    {
        $this->text = $text;
    }

    /**
     * Отображает только текст без каких-либо тегов BBCode.
     *
     * @param  string $text Необязательно: визуализируйте переданную строку BBCode вместо внутренней сохраненной.
     * @return string
     */
    public function renderPlain($text = null)
    {
        if ($this->text !== null and $text === null) {
            $text = $this->text;
        }

        return preg_replace("/\[(.*?)\]/is", '', $text);
    }

    /**
     * Отображает только текст без каких-либо тегов BBCode.
     * Alias for renderRaw().
     *
     * @deprecated Deprecated since 1.1.0
     *
     * @param  string $text Необязательно: визуализируйте переданную строку BBCode вместо внутренней сохраненной.
     * @return string
     */
    public function renderRaw($text = null)
    {
        return $this->renderPlain($text);
    }

    /**
     * Renders BBCode to HTML
     *
     * @param  string  $text      Необязательно: визуализируйте переданную строку BBCode вместо внутренней сохраненной.
     * @param  bool    $escape    Экранировать объекты HTML? (Только "<" и ">"!)
     * @param  bool    $keepLines Сохранить разрывы строк, заменив их на <br>?
     * @return string
     */
    public function render($text = null, $escape = true, $keepLines = true)
    {
        if ($this->text !== null and $text === null) {
            $text = $this->text;
        }

        $html     = '';
        $len      = mb_strlen($text);
        $inTag    = false;            // True if current position is inside a tag
        $inName   = false;            // True if current pos is inside a tag name
        $inStr    = false;            // True if current pos is inside a string
        /** @var Tag|null $tag */
        $tag      = null;
        $openTags = array();

        /** Перебрать каждый символ текста */
        for ($i = 0; $i < $len; $i++) {
            $char = mb_substr($text, $i, 1);

            if ($keepLines) {
                if ($char == "\n") {
                    $html .= '<br/>';
                }
                if ($char == "\r") {
                    continue;
                }
            }

            if (! $escape or ($char != '<' and $char != '>')) {
                /*
                 * $inTag == true означает, что текущая позиция находится внутри определения тега
                 * (= inside the brackets of a tag)
                 */
                if ($inTag) {
                    if ($char == '"') {
                        if ($inStr) {
                            $inStr = false;
                        } else {
                            if ($inName) {
                                $tag->valid = false;
                            } else {
                                $inStr = true;
                            }
                        }
                    } else {
                        /** Это закрывает тег */
                        if ($char == ']' and ! $inStr) {
                            $inTag  = false;
                            $inName = false;

                            if ($tag->valid) {
                                $code = null;

                                if ($tag->opening) {
                                    $code = $this->generateTag($tag, $html, null, $openTags);
                                } else {
                                    $openingTag = $this->popTag($openTags, $tag);
                                    if ($openingTag) {
                                        $code = $this->generateTag($tag, $html, $openingTag, $openTags);
                                    }
                                }

                                if ($code !== null and $tag->opening) {
                                    $openTags[$tag->name][] = $tag;
                                }

                                $html .= $code;
                            }
                            continue;
                        }

                        if ($inName and ! $inStr) {
                            /** Это делает текущий тег закрывающим тегом */
                            if ($char == '/') {
                                if ($tag->name) {
                                    $tag->valid = false;
                                } else {
                                    $tag->opening = false;
                                }
                            } else {
                                /** Это означает, что свойство начинается */
                                if ($char == '=') {
                                    if ($tag->name) {
                                        $inName = false;
                                    } else {
                                        $tag->valid = false;
                                    }
                                } else {
                                    $tag->name .= mb_strtolower($char);
                                }
                            }
                        } else { // Если мы не внутри имени, мы внутри свойства
                            $tag->property .= $char;
                        }
                    }
                } else {
                    /** Это открывает тег */
                    if ($char == '[') {
                        $inTag          = true;
                        $inName         = true;
                        $tag            = new Tag();
                        $tag->position  = mb_strlen($html);
                    } else {
                        $html .= $char;
                    }
                }
            } else {
                $html .= htmlspecialchars($char);
            }
        }

        /*
         * Check for tags that are not closed and close them.
         */
        foreach ($openTags as $name => $openTagsByType) {
            $closingTag = new Tag($name, false);

            foreach ($openTagsByType as $openTag) {
                $html .= $this->generateTag($closingTag, $html, $openTag);
            }
        }

        return $html;
    }

    /**
     * Generates and returns the HTML code of the current tag
     *
     * @param  Tag      $tag        The current tag
     * @param  string   $html       The current HTML code passed by reference - might be altered!
     * @param  Tag|null $openingTag The opening tag that is linked to the tag (or null)
     * @param  Tag[]    $openTags   Array with tags that are opned but not closed
     * @return string
     */
    protected function generateTag(Tag $tag, &$html, Tag $openingTag = null, array $openTags = [])
    {
        $code = null;

        if (in_array($tag->name, $this->ignoredTags)) {
            return $code;
        }

        switch ($tag->name) {
            case self::TAG_NAME_B:
                if ($tag->opening) {
                    $code = '<strong>';
                } else {
                    $code = '</strong>';
                }
                break;
            case self::TAG_NAME_I:
                if ($tag->opening) {
                    $code = '<em>';
                } else {
                    $code = '</em>';
                }
                break;
            case self::TAG_NAME_S:
                if ($tag->opening) {
                    $code = '<del>';
                } else {
                    $code = '</del>';
                }
                break;
            case self::TAG_NAME_U:
                if ($tag->opening) {
                    $code = '<span style="text-decoration: underline">';
                } else {
                    $code = '</span>';
                }
                break;
            case self::TAG_NAME_CODE:
                if ($tag->opening) {
                    $code = '<pre><code>';
                } else {
                    $code = '</code></pre>';
                }
                break;
            case self::TAG_NAME_EMAIL:
                if ($tag->opening) {
                    if ($tag->property) {
                        $code = '<a href="mailto:'.$tag->property.'">';
                    } else {
                        $code = '<a href="mailto:';
                    }
                } else {
                    if ($openingTag->property) {
                        $code = '</a>';
                    } else {
                        $code .= '">'.mb_substr($html, $openingTag->position + 16).'</a>';
                    }
                }
                break;
            case self::TAG_NAME_URL:
                if ($tag->opening) {
                    if ($tag->property) {
                        $code = '<a href="'.$tag->property.'">';
                    } else {
                        $code = '<a href="';
                    }
                } else {
                    if ($openingTag->property) {
                        $code = '</a>';
                    } else {
                        $partial = mb_substr($html, $openingTag->position + 9);
                        $html = mb_substr($html, 0, $openingTag->position + 9)
                            .strip_tags($partial).'">'.$partial.'</a>';
                    }
                }
                break;
            case self::TAG_NAME_IMG:
                if ($tag->opening) {
                    $code = '<img src="';
                } else {
                    $code = '" />';
                }
                break;
            case self::TAG_NAME_LIST:
                if ($tag->opening) {
                    $listType = '<ul>';

                    if ($tag->property) {
                        $listType = '<ol>';

                        if ($tag->property == 'i') {
                            $listType = '<ol style="list-style-type: lower-roman">';
                        } elseif ($tag->property == 'a') {
                            $listType = '<ol style="list-style-type: lower-alpha">';
                        }
                    }

                    $code = $listType;
                } else {
                    if ($this->endsWith($html, '<ul>')) {
                        $code = '</ul>';
                    } elseif ($this->endsWith($html, '<ol>')) {
                        $code = '</ol>';
                    } elseif ($this->endsWith($html, '<ol style="list-style-type: lower-roman">')) {
                        $code = '</ol>';
                    } elseif ($this->endsWith($html, '<ol style="list-style-type: lower-alpha">')) {
                        $code = '</ol>';
                    } elseif ($this->endsWith($html, '</li>') and $openingTag->property) {
                        $code = '</ol>';
                    } elseif ($this->endsWith($html, '</li>') and ! $openingTag->property) {
                        $code = '</ul>';
                    } elseif ($openingTag->property) {
                        $code = '</li></ol>';
                    } else {
                        $code = '</li></ul>';
                    }
                }
                break;
            case self::TAG_NAME_LI_STAR:
                if ($tag->opening) {
                    /*
                     * We require that the list item is inside a list
                     */
                    if (isset($openTags['list']) and sizeof($openTags['list']) > 0) {
                        $tag->opening = false;

                        if ($this->endsWith($html, '<ul>')) {
                            $code = '<li>';
                        } else {
                            $code = '</li><li>';
                        }
                    }
                }
                break;
            case self::TAG_NAME_LI:
                if ($tag->opening) {
                    /*
                     * We require that the list item is inside a list
                     */
                    if (isset($openTags['list']) and sizeof($openTags['list']) > 0) {
                        $code = '<li>';
                    }
                } else {
                    $code = '</li>';
                }
                break;
            case self::TAG_NAME_QUOTE:
                if ($tag->opening) {
                    if ($tag->property) {
                        $code = '<blockquote><span class="author">'.$tag->property.':</span><br/>';
                    } else {
                        $code = '<blockquote>';
                    }
                } else {
                    $code = '</blockquote>';
                }
                break;
            case self::TAG_NAME_YOUTUBE:
                if ($tag->opening) {
                    $code = '<iframe class="youtube-player" type="text/html" width="' . $this->youTubeWidth . '"\
                        height="' . $this->youTubeHeight . '" src="https://www.youtube.com/embed/';
                } else {
                    $code = '" frameborder="0"></iframe>';
                }
                break;
//            case self::TAG_NAME_VIDEO:
//
//                $code = preg_replace_callback( "#\[video\s*=\s*(\S.+?)\s*\]#i", array( &$tag->property, 'build_video'), $code );
//                break;
            case self::TAG_NAME_FONT:
                if ($tag->opening) {
                    if ($tag->property) {
                        $code = '<span style="font-family: '.$tag->property.'">';
                    }
                } else {
                    $code = '</span>';
                }
                break;
            case self::TAG_NAME_SIZE:
                if ($tag->opening) {
                    if ($tag->property) {
                        $code = '<span style="font-size: '.$tag->property.'%">';
                    }
                } else {
                    $code = '</span>';
                }
                break;
            case self::TAG_NAME_COLOR:
                if ($tag->opening) {
                    if ($tag->property) {
                        $code = '<span style="color: '.$tag->property.'">';
                    }
                } else {
                    $code = '</span>';
                }
                break;
            case self::TAG_NAME_LEFT:
                if ($tag->opening) {
                    $code = '<div style="text-align: left">';
                } else {
                    $code = '</div>';
                }
                break;
            case self::TAG_NAME_CENTER:
                if ($tag->opening) {
                    $code = '<div style="text-align: center">';
                } else {
                    $code = '</div>';
                }
                break;
            case self::TAG_NAME_RIGHT:
                if ($tag->opening) {
                    $code = '<div style="text-align: right">';
                } else {
                    $code = '</div>';
                }
                break;
            case self::TAG_NAME_SPOILER:
                if ($tag->opening) {
                    $code = '<div class="spoiler">';
                } else {
                    $code = '</div>';
                }
                break;
            default:
                // Custom tags:
                foreach ($this->customTagClosures as $name => $closure) {
                    if ($tag->name === $name) {
                        $code .= $closure($tag, $html, $openingTag);
                    }
                }
        }

        return $code;
    }

    /**
     * Magic method __toString()
     *
     * @return string
     */
    public function __toString()
    {
        return $this->render();
    }

    /**
     * Returns the last tag of a given type and removes it from the array.
     *
     * @param  Tag[]    $tags Array of tags
     * @param  Tag      $tag  Return the last tag of the type of this tag
     * @return Tag|null
     */
    protected function popTag(array &$tags, $tag)
    {
        if (! isset($tags[$tag->name])) {
            return null;
        }

        $size = sizeof($tags[$tag->name]);

        if ($size === 0) {
            return null;
        } else {
            return array_pop($tags[$tag->name]);
        }
    }

    /**
     * Adds a custom tag (with name and a Closure)
     *
     * Example:
     *
     * $bbcode->addTag('example', function($tag, &$html, $openingTag) {
     *     if ($tag->opening) {
     *         return '<span class="example">';
     *     } else {
     *         return '</span>';
     *     }
     * });
     *
     * @param string  $name    The name of the tag
     * @param Closure $closure The Closure that renders the tag
     * @return void
     */
    public function addTag($name, Closure $closure)
    {
        $this->customTagClosures[$name] = $closure;
    }

    /**
     * Remove the custom tag with the given name
     *
     * @param  string $name
     * @return void
     */
    public function forgetTag($name)
    {
        unset($this->customTagClosures[$name]);
    }

    /**
     * Add a tag to the array of ignored tags
     *
     * @param  string $name The name of the tag
     * @return void
     */
    public function ignoreTag($name)
    {
        if (! in_array($name, $this->ignoredTags)) {
            $this->ignoredTags[] = $name;
        }
    }

    /**
     * Remove a tag from the array of ignored tags
     *
     * @param  string $name The name of the tag
     * @return void
     */
    public function permitTag($name)
    {
        $key = array_search($name, $this->ignoredTags);

        if ($key !== false) {
            unset($this->ignoredTags[$key]);
        }
    }

    /**
     * Returns an array with the name of the tags that are ignored
     *
     * @return string[]
     */
    public function getIgnoredTags()
    {
        return $this->ignoredTags;
    }

    /**
     * Get the width of the YouTube iframe element
     *
     * @return int
     */
    public function getYouTubeWidth()
    {
        return $this->youTubeWidth;
    }

    /**
     * Set the width of the YouTube iframe element
     *
     * @param int $youTubeWidth
     * @return void
     */
    public function setYouTubeWidth($youTubeWidth)
    {
        $this->youTubeWidth = $youTubeWidth;
    }

    /**
     * Get the height of the YouTube iframe element
     *
     * @return int
     */
    public function getYouTubeHeight()
    {
        return $this->youTubeHeight;
    }

    /**
     * Set the height of the YouTube iframe element
     *
     * @param int $youTubeHeight
     * @return void
     */
    public function setYouTubeHeight($youTubeHeight)
    {
        $this->youTubeHeight = $youTubeHeight;
    }

    /**
     * Returns an array with the names of all default tags
     *
     * @return string[]
     */
    public function getDefaultTagNames()
    {
        return [
            self::TAG_NAME_B,
            self::TAG_NAME_I,
            self::TAG_NAME_S,
            self::TAG_NAME_U,
            self::TAG_NAME_CODE,
            self::TAG_NAME_EMAIL,
            self::TAG_NAME_URL,
            self::TAG_NAME_IMG,
            self::TAG_NAME_LIST,
            self::TAG_NAME_LI_STAR,
            self::TAG_NAME_LI,
            self::TAG_NAME_QUOTE,
            self::TAG_NAME_YOUTUBE,
            self::TAG_NAME_FONT,
            self::TAG_NAME_SIZE,
            self::TAG_NAME_COLOR,
            self::TAG_NAME_LEFT,
            self::TAG_NAME_CENTER,
            self::TAG_NAME_RIGHT,
            self::TAG_NAME_SPOILER,
        ];
    }

    /**
     * Returns true if $haystack ends with $needle
     *
     * @param  string $haystack
     * @param  string $needle
     * @return bool
     */
    protected function endsWith($haystack, $needle)
    {
        return ($needle === '' or mb_substr($haystack, -mb_strlen($needle)) === $needle);
    }

    function build_video( $matches = array() )
    {

        $url = $matches[1];

        $get_size = array();
        $sizes = array();
        $get_size = explode( ",", trim( $url ) );

        if (count($get_size) > 1 AND ( stripos ( $get_size[0], "http" ) === false OR stripos ( $get_size[0], "rtmp:" ) === false ) )  {

            $sizes = explode( "x", trim( $get_size[0]));

            if (count($sizes) == 2) {
                $width = intval($sizes[0]) > 0 ? intval($sizes[0]) : $this->video_config['width'];
                $height = intval($sizes[1]) > 0 ? intval($sizes[1]) : $this->video_config['height'];

                if (substr ( $sizes[0], - 1, 1 ) == '%') $width = $width."%";
                if (substr ( $sizes[1], - 1, 1 ) == '%') $height = $height."%";

            } else {
                $width = $this->video_config['width'];
                $height = $this->video_config['height'];
            }
        } else {
            $width = $this->video_config['width'];
            $height = $this->video_config['height'];
        }

        if (count($get_size) == 3) {
            $url = $get_size[1].",".$get_size[2];
        } elseif (count($get_size) == 2 AND count($sizes) == 2) {
            $url = $get_size[1];
        }

        $option = explode( "|", trim( $url ) );

        $url = $this->clear_url( $option[0] );

        $type = explode( ".", $url );
        $type = strtolower( end( $type ) );

        if (preg_match( "/[?&;<\[\]]/", $url )) {
            return "[video=" . $url . "]";
        }

        if ( $option[1] != "" ) {
            $option[1] = htmlspecialchars( strip_tags( stripslashes( $option[1] ) ), ENT_QUOTES, Config::getValue('charset'));
            $decode_url = $url . "|" . $option[1];
        } else {
            $decode_url = $url;
        }

        if (count($sizes) == 2 ) $decode_url = $width. "x" . $height . ",".$decode_url;

        $detect_rtmp = !((stripos($url, "rtmp:") === false));

        if ( $type == "mp4" or $type == "m4v" or $type == "m4a" or $type == "mov"  or $detect_rtmp) {

            $this->video_config['buffer'] = intval($this->video_config['buffer']);
            $this->video_config['fullsizeview'] = intval($this->video_config['fullsizeview']);

            $list = explode( ",", $url );
            $url = trim($list[0]);

            $url_hd = (count($list) > 1) ? trim($list[1]) : '';

            if( $type == "mp4" AND $this->video_config['use_html5'] ) {

                $preview = $option[1] != "" ? $option[1] : "";

                return "<!--video_begin:{$decode_url}--><video width=\"{$width}\" height=\"{$height}\" preload=\"metadata\" poster=\"{$preview}\" controls=\"controls\">
						<source type=\"video/mp4\" src=\"{$url}\"></source>
						</video><!--dle_video_end-->";
            }

        } else {

            $url = htmlspecialchars( trim( $url ) , ENT_QUOTES, Config::getValue('charset') );

            return "<!--video_begin:{$decode_url}--><object id=\"mediaPlayer\" width=\"{$width}\" height=\"{$height}\" classid=\"CLSID:6BF52A52-394A-11d3-B153-00C04F79FAA6\" standby=\"Loading Microsoft Windows Media Player components...\" type=\"application/x-oleobject\">
				<param name=\"url\" VALUE=\"{$url}\" />
				<param name=\"autoStart\" VALUE=\"{$this->video_config['play']}\" />
				<param name=\"showControls\" VALUE=\"true\" />
				<param name=\"TransparentatStart\" VALUE=\"false\" />
				<param name=\"AnimationatStart\" VALUE=\"true\" />
				<param name=\"StretchToFit\" VALUE=\"true\" />
				<embed pluginspage=\"http://www.microsoft.com/Windows/Downloads/Contents/MediaPlayer/\" src=\"{$url}\" width=\"{$width}\" height=\"{$height}\" type=\"application/x-mplayer2\" autorewind=\"1\" showstatusbar=\"1\" showcontrols=\"1\" autostart=\"{$this->video_config['play']}\" allowchangedisplaysize=\"1\" volume=\"70\" stretchtofit=\"1\"></embed>
				</object><!--dle_video_end-->";
        }

    }

    function clear_url($url)
    {

        $url = strip_tags( trim( stripslashes( $url ) ) );

        $url = str_replace( '\"', '"', $url );
        $url = str_replace( "'", "", $url );
        $url = str_replace( '"', "", $url );
        $url = htmlspecialchars( $url, ENT_QUOTES, Config::getValue('charset') );


        $url = str_ireplace( "document.cookie", "d&#111;cument.cookie", $url );
        $url = str_replace( " ", "%20", $url );
        $url = str_replace( "<", "&#60;", $url );
        $url = str_replace( ">", "&#62;", $url );
        $url = preg_replace( "/javascript:/i", "j&#097;vascript:", $url );
        $url = preg_replace( "/data:/i", "d&#097;ta:", $url );

        return $url;

    }
}