<?php

namespace bizley\podium\models;

use bizley\podium\db\Query;
use bizley\podium\log\Log;
use bizley\podium\models\db\PostActiveRecord;
use bizley\podium\Podium;
use bizley\podium\PodiumCache;
use cebe\markdown\GithubMarkdown;
use Exception;
use yii\data\ActiveDataProvider;
use yii\helpers\HtmlPurifier;

/**
 * Post model
 *
 * @author Paweł Bizley Brzozowski <pawel@positive.codes>
 * @since 0.1
 *
 * @property string $parsedContent
 */
class Post extends PostActiveRecord
{
    /**
     * Returns latest posts for registered users.
     * @param int $limit Number of latest posts.
     * @return Post[]
     */
    public static function getLatestPostsForMembers($limit = 5)
    {
        return static::find()->orderBy(['created_at' => SORT_DESC])->limit($limit)->all();
    }

    /**
     * Returns latest visible posts for guests.
     * @param int $limit Number of latest posts.
     * @return Post[]
     */
    public static function getLatestPostsForGuests($limit = 5)
    {
        return static::find()->joinWith(['forum' => function ($query) {
            $query->andWhere([Forum::tableName() . '.visible' => 1])->joinWith(['category' => function ($query) {
                $query->andWhere([Category::tableName() . '.visible' => 1]);
            }]);
        }])->orderBy(['created_at' => SORT_DESC])->limit($limit)->all();
    }

    /**
     * Updates post tag words.
     * @param bool $insert
     * @param array $changedAttributes
     * @throws Exception
     */
    public function afterSave($insert, $changedAttributes)
    {
        try {
            if ($insert) {
                $this->insertWords();
            } else {
                $this->updateWords();
            }
        } catch (Exception $e) {
            throw $e;
        }
        parent::afterSave($insert, $changedAttributes);
    }

    /**
     * Prepares tag words.
     * @return string[]
     */
    protected function prepareWords()
    {
        $cleanHtml = HtmlPurifier::process(trim($this->content));
        $purged = preg_replace('/<[^>]+>/', ' ', $cleanHtml);
        $wordsRaw = array_unique(preg_split('/[\s,\.\n]+/', $purged));
        $allWords = [];
        foreach ($wordsRaw as $word) {
            if (mb_strlen($word, 'UTF-8') > 2 && mb_strlen($word, 'UTF-8') <= 255) {
                $allWords[] = $word;
            }
        }
        return $allWords;
    }

    /**
     * Adds new tag words.
     * @param string[] $allWords All words extracted from post
     * @throws Exception
     */
    protected function addNewWords($allWords)
    {
        try {
            $newWords = $allWords;
            $query = (new Query())->from(Vocabulary::tableName())->where(['word' => $allWords]);
            foreach ($query->each() as $vocabularyFound) {
                if (($key = array_search($vocabularyFound['word'], $allWords)) !== false) {
                    unset($newWords[$key]);
                }
            }
            $formatWords = [];
            foreach ($newWords as $word) {
                $formatWords[] = [$word];
            }
            if (!empty($formatWords)) {
                if (!Podium::getInstance()->db->createCommand()->batchInsert(
                        Vocabulary::tableName(), ['word'], $formatWords
                    )->execute()) {
                    throw new Exception('Words saving error!');
                }
            }
        } catch (Exception $e) {
            Log::error($e->getMessage(), null, __METHOD__);
            throw $e;
        }
    }

    /**
     * Inserts tag words.
     * @throws Exception
     */
    protected function insertWords()
    {
        try {
            $vocabulary = [];
            $allWords = $this->prepareWords();
            $this->addNewWords($allWords);
            $query = (new Query())->from(Vocabulary::tableName())->where(['word' => $allWords]);
            foreach ($query->each() as $vocabularyNew) {
                $vocabulary[] = [$vocabularyNew['id'], $this->id];
            }
            if (!empty($vocabulary)) {
                if (!Podium::getInstance()->db->createCommand()->batchInsert(
                        '{{%podium_vocabulary_junction}}', ['word_id', 'post_id'], $vocabulary
                    )->execute()) {
                    throw new Exception('Words connections saving error!');
                }
            }
        } catch (Exception $e) {
            Log::error($e->getMessage(), null, __METHOD__);
            throw $e;
        }
    }

    /**
     * Updates tag words.
     * @throws Exception
     */
    protected function updateWords()
    {
        try {
            $vocabulary = [];
            $allWords = $this->prepareWords();
            $this->addNewWords($allWords);
            $queryVocabulary = (new Query())->from(Vocabulary::tableName())->where(['word' => $allWords]);
            foreach ($queryVocabulary->each() as $vocabularyNew) {
                $vocabulary[$vocabularyNew['id']] = [$vocabularyNew['id'], $this->id];
            }
            if (!empty($vocabulary)) {
                if (!Podium::getInstance()->db->createCommand()->batchInsert(
                        '{{%podium_vocabulary_junction}}', ['word_id', 'post_id'], array_values($vocabulary)
                    )->execute()) {
                    throw new Exception('Words connections saving error!');
                }
            }
            $queryJunction = (new Query())->from('{{%podium_vocabulary_junction}}')->where(['post_id' => $this->id]);
            foreach ($queryJunction->each() as $junk) {
                if (!array_key_exists($junk['word_id'], $vocabulary)) {
                    if (!Podium::getInstance()->db->createCommand()->delete(
                            '{{%podium_vocabulary_junction}}', ['id' => $junk['id']]
                        )->execute()) {
                        throw new Exception('Words connections deleting error!');
                    }
                }
            }
        } catch (Exception $e) {
            Log::error($e->getMessage(), null, __METHOD__);
            throw $e;
        }
    }

    /**
     * Verifies if user is this forum's moderator.
     * @param int|null $userId user ID or null for current signed in.
     * @return bool
     */
    public function isMod($userId = null)
    {
        return $this->forum->isMod($userId);
    }

    /**
     * Searches for posts.
     * @param int $forumId
     * @param int $threadId
     * @return ActiveDataProvider
     */
    public function search($forumId, $threadId)
    {
        $dataProvider = new ActiveDataProvider([
            'query' => static::find()->where(['forum_id' => $forumId, 'thread_id' => $threadId]),
            'pagination' => [
                'defaultPageSize' => 10,
                'pageSizeLimit' => false,
                'forcePageParam' => false
            ],
        ]);
        $dataProvider->sort->defaultOrder = ['id' => SORT_ASC];
        return $dataProvider;
    }

    /**
     * Searches for posts added by given user.
     * @param int $userId
     * @return ActiveDataProvider
     */
    public function searchByUser($userId)
    {
        $query = static::find();
        $query->where(['author_id' => $userId]);
        if (Podium::getInstance()->user->isGuest) {
            $query->joinWith(['forum' => function ($q) {
                $q->where([Forum::tableName() . '.visible' => 1]);
            }]);
        }

        $dataProvider = new ActiveDataProvider([
            'query' => $query,
            'pagination' => [
                'defaultPageSize' => 10,
                'pageSizeLimit' => false,
                'forcePageParam' => false
            ],
        ]);
        $dataProvider->sort->defaultOrder = ['id' => SORT_ASC];

        return $dataProvider;
    }

    /**
     * Marks post as seen by current user.
     * @param bool $updateCounters Whether to update view counter
     */
    public function markSeen($updateCounters = true)
    {
        if (!Podium::getInstance()->user->isGuest) {
            $transaction = static::getDb()->beginTransaction();
            try {
                $loggedId = User::loggedId();
                $threadView = ThreadView::find()->where([
                        'user_id' => $loggedId,
                        'thread_id' => $this->thread_id
                    ])->limit(1)->one();

                if (empty($threadView)) {
                    $threadView = new ThreadView();
                    $threadView->user_id = $loggedId;
                    $threadView->thread_id = $this->thread_id;
                    $threadView->new_last_seen = $this->created_at;
                    $threadView->edited_last_seen = !empty($this->edited_at) ? $this->edited_at : $this->created_at;
                    if (!$threadView->save()) {
                        throw new Exception('Thread View saving error!');
                    }
                    if ($updateCounters) {
                        if (!$this->thread->updateCounters(['views' => 1])) {
                            throw new Exception('Thread views adding error!');
                        }
                    }
                } else {
                    if ($this->edited) {
                        if ($threadView->edited_last_seen < $this->edited_at) {
                            $threadView->edited_last_seen = $this->edited_at;
                            if (!$threadView->save()) {
                                throw new Exception('Thread View saving error!');
                            }
                            if ($updateCounters) {
                                if (!$this->thread->updateCounters(['views' => 1])) {
                                    throw new Exception('Thread views adding error!');
                                }
                            }
                        }
                    } else {
                        $save = false;
                        if ($threadView->new_last_seen < $this->created_at) {
                            $threadView->new_last_seen = $this->created_at;
                            $save = true;
                        }
                        if ($threadView->edited_last_seen < max($this->created_at, $this->edited_at)) {
                            $threadView->edited_last_seen = max($this->created_at, $this->edited_at);
                            $save = true;
                        }
                        if ($save) {
                            if (!$threadView->save()) {
                                throw new Exception('Thread View saving error!');
                            }
                            if ($updateCounters) {
                                if (!$this->thread->updateCounters(['views' => 1])) {
                                    throw new Exception('Thread views adding error!');
                                }
                            }
                        }
                    }
                }
                if ($this->thread->subscription) {
                    if ($this->thread->subscription->post_seen == Subscription::POST_NEW) {
                        $this->thread->subscription->post_seen = Subscription::POST_SEEN;
                        if (!$this->thread->subscription->save()) {
                            throw new Exception('Thread Subscription saving error!');
                        }
                    }
                }
                $transaction->commit();
            } catch (Exception $e) {
                $transaction->rollBack();
                Log::error($e->getMessage(), null, __METHOD__);
            }
        }
    }

    /**
     * Returns latest post.
     * @param int $limit
     * @return array
     */
    public static function getLatest($limit = 5)
    {
        $cacheKey = Podium::getInstance()->user->isGuest ? 'guest' : 'member';
        $method = Podium::getInstance()->user->isGuest ? 'getLatestPostsForGuests' : 'getLatestPostsForMembers';
        $latest = Podium::getInstance()->podiumCache->getElement('forum.latestposts', $cacheKey);
        if ($latest === false) {
            $posts = static::$method($limit);
            foreach ($posts as $post) {
                $latest[] = [
                    'id' => $post->id,
                    'title' => $post->thread->name,
                    'created' => $post->created_at,
                    'author' => $post->author->podiumTag
                ];
            }
            Podium::getInstance()->podiumCache->setElement('forum.latestposts', $cacheKey, $latest);
        }
        return $latest;
    }

    /**
     * Returns the verified post.
     * @param int $categoryId post category ID
     * @param int $forumId post forum ID
     * @param int $threadId post thread ID
     * @param int $id post ID
     * @return Post
     * @since 0.2
     */
    public static function verify($categoryId = null, $forumId = null, $threadId = null, $id = null)
    {
        if (!is_numeric($categoryId) || $categoryId < 1
                || !is_numeric($forumId) || $forumId < 1
                || !is_numeric($threadId) || $threadId < 1
                || !is_numeric($id) || $id < 1) {
            return null;
        }
        return static::find()->joinWith(['thread', 'forum' => function ($query) use ($categoryId) {
                $query->joinWith(['category'])->andWhere([Category::tableName() . '.id' => $categoryId]);
            }])->where([
                static::tableName() . '.id' => $id,
                static::tableName() . '.thread_id' => $threadId,
                static::tableName() . '.forum_id' => $forumId,
            ])->limit(1)->one();
    }

    /**
     * Performs post delete with parent forum and thread counters update.
     * @return bool
     * @since 0.2
     */
    public function podiumDelete()
    {
        $transaction = static::getDb()->beginTransaction();
        try {
            if (!$this->delete()) {
                throw new Exception('Post deleting error!');
            }
            $wholeThread = false;
            if ($this->thread->postsCount) {
                if (!$this->thread->updateCounters(['posts' => -1])) {
                    throw new Exception('Thread Post counter subtracting error!');
                }
                if (!$this->forum->updateCounters(['posts' => -1])) {
                    throw new Exception('Forum Post counter subtracting error!');
                }
            } else {
                $wholeThread = true;
                if (!$this->thread->delete()) {
                    throw new Exception('Thread deleting error!');
                }
                if (!$this->forum->updateCounters(['posts' => -1, 'threads' => -1])) {
                    throw new Exception('Forum Post and Thread counter subtracting error!');
                }
            }
            $transaction->commit();
            PodiumCache::clearAfter('postDelete');
            Log::info('Post deleted', !empty($this->id) ? $this->id : '', __METHOD__);
            return true;
        } catch (Exception $e) {
            $transaction->rollBack();
            Log::error($e->getMessage(), null, __METHOD__);
        }
        return false;
    }

    /**
     * Performs post update with parent thread topic update in case of first post in thread.
     * @param bool $isFirstPost whether post is first in thread
     * @return bool
     * @since 0.2
     */
    public function podiumEdit($isFirstPost = false)
    {
        $transaction = static::getDb()->beginTransaction();
        try {
            $this->edited = 1;
            $this->touch('edited_at');
            if (!$this->save()) {
                throw new Exception('Post saving error!');
            }
            if ($isFirstPost) {
                $this->thread->name = $this->topic;
                if (!$this->thread->save()) {
                    throw new Exception('Thread saving error!');
                }
            }
            $this->markSeen();
            $this->thread->touch('edited_post_at');

            $transaction->commit();
            Log::info('Post updated', $this->id, __METHOD__);
            return true;
        } catch (Exception $e) {
            $transaction->rollBack();
            Log::error($e->getMessage(), null, __METHOD__);
        }
        return false;
    }

    /**
     * Performs new post creation and subscription.
     * Depending on the settings previous post can be merged.
     * @param Post $previous previous post
     * @return bool
     * @since 0.2
     */
    public function podiumNew($previous = null)
    {
        $transaction = static::getDb()->beginTransaction();
        try {
            $id = null;
            $loggedId = User::loggedId();
            $sameAuthor = !empty($previous->author_id) && $previous->author_id == $loggedId;
            if ($sameAuthor && Podium::getInstance()->podiumConfig->get('merge_posts')) {
                $separator = '<hr>';
                if (Podium::getInstance()->podiumConfig->get('use_wysiwyg') == '0') {
                    $separator = "\n\n---\n";
                }
                $previous->content .= $separator . $this->content;
                $previous->edited = 1;
                $previous->touch('edited_at');
                if (!$previous->save()) {
                    throw new Exception('Previous Post saving error!');
                }
                $previous->markSeen(false);
                $previous->thread->touch('edited_post_at');
                $id = $previous->id;
                $thread = $previous->thread;
            } else {
                if (!$this->save()) {
                    throw new Exception('Post saving error!');
                }
                $this->markSeen(!$sameAuthor);
                if (!$this->forum->updateCounters(['posts' => 1])) {
                    throw new Exception('Forum Post counter adding error!');
                }
                if (!$this->thread->updateCounters(['posts' => 1])) {
                    throw new Exception('Thread Post counter adding error!');
                }
                $this->thread->touch('new_post_at');
                $this->thread->touch('edited_post_at');
                $id = $this->id;
                $thread = $this->thread;
            }
            if (empty($id)) {
                throw new Exception('Saved Post ID missing');
            }
            Subscription::notify($thread->id);
            if ($this->subscribe && !$thread->subscription) {
                $subscription = new Subscription();
                $subscription->user_id = $loggedId;
                $subscription->thread_id = $thread->id;
                $subscription->post_seen = Subscription::POST_SEEN;
                if (!$subscription->save()) {
                    throw new Exception('Subscription saving error!');
                }
            }
            $transaction->commit();
            PodiumCache::clearAfter('newPost');
            Log::info('Post added', $id, __METHOD__);
            return true;
        } catch (Exception $e) {
            $transaction->rollBack();
            Log::error($e->getMessage(), null, __METHOD__);
        }
        return false;
    }

    /**
     * Performs vote processing.
     * @param bool $up whether this is upvote
     * @param int $count number of user cached votes
     * @return bool
     * @since 0.2
     */
    public function podiumThumb($up = true, $count = 0)
    {
        $transaction = static::getDb()->beginTransaction();
        try {
            $loggedId = User::loggedId();
            if ($this->thumb) {
                if ($this->thumb->thumb == 1 && !$up) {
                    $this->thumb->thumb = -1;
                    if (!$this->thumb->save()) {
                        throw new Exception('Thumb saving error!');
                    }
                    if (!$this->updateCounters(['likes' => -1, 'dislikes' => 1])) {
                        throw new Exception('Thumb counters saving error!');
                    }
                } elseif ($this->thumb->thumb == -1 && $up) {
                    $this->thumb->thumb = 1;
                    if (!$this->thumb->save()) {
                        throw new Exception('Thumb saving error!');
                    }
                    if (!$this->updateCounters(['likes' => 1, 'dislikes' => -1])) {
                        throw new Exception('Thumb counters saving error!');
                    }
                }
            } else {
                $postThumb = new PostThumb();
                $postThumb->post_id = $this->id;
                $postThumb->user_id = $loggedId;
                $postThumb->thumb = $up ? 1 : -1;
                if (!$postThumb->save()) {
                    throw new Exception('PostThumb saving error!');
                }
                if ($postThumb->thumb) {
                    if (!$this->updateCounters(['likes' => 1])) {
                        throw new Exception('Thumb counters saving error!');
                    }
                } else {
                    if (!$this->updateCounters(['dislikes' => 1])) {
                        throw new Exception('Thumb counters saving error!');
                    }
                }
            }
            if ($count == 0) {
                Podium::getInstance()->podiumCache->set('user.votes.' . $loggedId, ['count' => 1, 'expire' => time() + 3600]);
            } else {
                Podium::getInstance()->podiumCache->setElement('user.votes.' . $loggedId, 'count', $count + 1);
            }
            $transaction->commit();
            return true;
        } catch (Exception $e) {
            $transaction->rollBack();
            Log::error($e->getMessage(), null, __METHOD__);
        }
        return false;
    }

    /**
     * Returns content Markdown-parsed if WYSIWYG editor is switched off.
     * @return string
     * @since 0.6
     */
    public function getParsedContent()
    {
        if (Podium::getInstance()->podiumConfig->get('use_wysiwyg') == '0') {
            $parser = new GithubMarkdown();
            $parser->html5 = true;
            return $parser->parse($this->content);
        }
        return $this->content;
    }
}
