<?php

/**
 * @link https://www.humhub.org/
 * @copyright Copyright (c) 2017 HumHub GmbH & Co. KG
 * @license https://www.humhub.com/licences
 */

namespace humhub\components;

use Exception;
use humhub\components\behaviors\PolymorphicRelation;
use humhub\modules\content\components\ContentContainerActiveRecord;
use humhub\modules\content\interfaces\ContentOwner;
use humhub\modules\comment\models\Comment;
use humhub\modules\content\models\Content;
use humhub\modules\content\widgets\richtext\converter\RichTextToPlainTextConverter;
use humhub\modules\content\widgets\richtext\converter\RichTextToShortTextConverter;
use humhub\modules\user\models\User;
use humhub\modules\space\models\Space;
use Yii;
use yii\base\BaseObject;
use yii\helpers\Html;
use yii\helpers\Json;
use yii\helpers\Url;

/**
 * This class represents a social Activity triggered within the network.
 *
 * A SocialActivity can be assigned with an originator User, which triggered the activity and a source ActiveRecord.
 * The source is used to connect the SocialActivity to a related Content, ContentContainerActiveRecord or any other
 * ActiveRecord.
 *
 * Since SocialActivities need to be rendered in most cases it implements the humhub\components\rendering\Viewable interface and provides
 * a default implementation of the getViewParams function.
 *
 * @since 1.1
 * @author buddha
 */
abstract class SocialActivity extends BaseObject implements rendering\Viewable
{
    /**
     * User which performed the activity.
     *
     * @var \humhub\modules\user\models\User
     */
    public $originator;

    /**
     * @var bool ensure originator existence
     * @since 1.3
     */
    public $requireOriginator = true;

    /**
     * The source instance which created this activity
     *
     * @var ActiveRecord
     */
    public $source;

    /**
     * @var bool ensure source existence
     * @since 1.3
     */
    public $requireSource = true;

    /**
     * @var string the module id which this activity belongs to (required)
     */
    public $moduleId;

    /**
     * An SocialActivity can be represented in the database as ActiveRecord.
     * By defining the $recordClass an ActiveRecord will be created automatically within the
     * init function.
     *
     * @var ActiveRecord The related record for this activitiy
     */
    public $record;

    /**
     * @var string Record class used for instantiation.
     */
    public $recordClass;

    /**
     * @var string view name used for rendering the activity
     */
    public $viewName = 'default.php';

    /**
     * @inheritdoc
     */
    public function init()
    {
        parent::init();

        if ($this->recordClass) {
            $this->record = Yii::createObject($this->recordClass);
            $this->record->class = get_class($this);
            $this->record->module = $this->moduleId;
        }
    }

    /**
     * Static initializer should be prefered over new initialization, since it makes use
     * of Yii::createObject dependency injection/configuration.
     *
     * @param array $options
     * @return static
     * @throws \yii\base\InvalidConfigException
     */
    public static function instance($options = [])
    {
        return Yii::createObject(static::class, $options);
    }

    /**
     * Builder function for the originator.
     *
     * @param User $originator
     * @return \humhub\components\SocialActivity
     */
    public function from($originator)
    {
        $this->originator = $originator;

        return $this;
    }

    /**
     * Builder function for the source.
     * @param ActiveRecord $source
     * @return $this
     */
    public function about($source)
    {
        $this->source = $source;
        $this->record->setPolymorphicRelation($source);

        return $this;
    }

    /**
     * @inheritdoc
     */
    public function getViewName()
    {
        // If no suffix is given, we assume a php file.
        if (!strpos($this->viewName, '.')) {
            return $this->viewName . '.php';
        } else {
            return $this->viewName;
        }
    }

    /**
     * @inheritdoc
     */
    public function getViewParams($params = [])
    {
        $result = [
            'originator' => $this->originator,
            'source' => $this->source,
            'contentContainer' => $this->getContentContainer(),
            'space' => $this->getSpace(),
            'record' => $this->record,
            'url' => $this->getUrl(),
            'viewable' => $this,
            'html' => $this->html(),
            'text' => $this->text(),
        ];

        return \yii\helpers\ArrayHelper::merge($result, $params);
    }

    /**
     * Returns the related content instance in case the source is of type ContentOwner.
     *
     * @return \humhub\modules\content\models\Content Content ActiveRecord or null if not related to a ContentOwner source
     */
    public function getContent()
    {
        if ($this->hasContent()) {
            return $this->source->content;
        }

        return null;
    }

    /**
     * @return Space related space instance in case the activity source is an related contentcontainer of type space, otherwise null
     * @throws \yii\base\Exception
     */
    public function getSpace()
    {
        $container = $this->getContentContainer();

        return ($container instanceof Space) ? $container : null;
    }

    /**
     * @return int related space id in case the activity source is an related contentcontainer of type space, otherwise null
     * @throws \yii\base\Exception
     */
    public function getSpaceId()
    {
        $space = $this->getSpace();

        return ($space) ? $space->id : null;
    }

    /**
     * Determines if this activity is related to a content. This is the case if the activitiy source
     * is of type ContentOwner.
     *
     * @return bool true if this activity is related to a ContentOwner else false
     */
    public function hasContent()
    {
        return $this->source instanceof ContentOwner;
    }

    /**
     * Determines if the activity source is related to an ContentContainer.
     * This is the case if the source is either a ContentContainerActiveRecord itself or a ContentOwner.
     *
     * @return ContentContainerActiveRecord
     * @throws \yii\base\Exception
     */
    public function getContentContainer()
    {
        if ($this->source instanceof ContentContainerActiveRecord) {
            return $this->source;
        } elseif ($this->hasContent()) {
            return $this->getContent()->getContainer();
        }

        return null;
    }

    /**
     * Url of the origin of this notification
     * If source is a Content / ContentAddon / ContentContainer this will automatically generated.
     * NOTE: Returned URL must be absolute with scheme
     *
     * @return string
     */
    public function getUrl()
    {
        $url = '#';

        if ($this->source instanceof Comment) {
            $url = $this->source->getUrl();
        } elseif ($this->hasContent()) {
            $url = $this->getContent()->getUrl(true);
        } elseif ($this->source instanceof ContentContainerActiveRecord) {
            $url = $this->source->getUrl(true);
        }

        // Create absolute URL, for E-Mails
        if (substr($url, 0, 4) !== 'http') {
            $url = Url::to($url, true);
        }

        return $url;
    }

    /**
     * @inheritdoc
     */
    public function text()
    {
        $html = $this->html();

        return !empty($html) ? html_entity_decode(strip_tags($html)) : null;
    }

    /**
     * @inheritdoc
     */
    public function html()
    {
        return null;
    }

    /**
     * @inheritdoc
     */
    public function json()
    {
        return Json::encode($this->asArray());
    }

    /**
     * Returns an array representation of this notification.
     */
    public function asArray(User $user)
    {
        $result = [
            'class' => get_class($this),
            'text' => $this->text(),
            'html' => $this->html(),
        ];

        if ($this->originator) {
            $result['originator_id'] = $this->originator->id;
        }

        if ($this->source) {
            $result['source_class'] = PolymorphicRelation::getObjectModel($this->source);
            $result['source_pk'] = $this->source->getPrimaryKey();
            $result['space_id'] = $this->source->getSpaceId();
        }

        return $result;
    }

    /**
     * Build info text about a content
     *
     * This is a combination of the type of the content with a short preview
     * of it.
     *
     * If no $content is provided the contentInfo of $source is returned.
     *
     * @param Content $content
     * @return string|null
     * @throws Exception
     */
    public function getContentInfo(ContentOwner $content = null, $withContentName = true)
    {
        if (!$this->hasContent() && !$content) {
            return null;
        }

        if (!$content) {
            $content = $this->source;
        }

        $info = $this->getContentPreview($content, 60);

        if (empty($info)) {
            return null;
        }

        return ($withContentName) ? Html::encode($content->getContentName()) . ' "' . $info . '"' : $info;
    }

    /**
     * Returns a short preview text of the content. The max length can be defined by setting
     * $maxLength (60 by default).
     *
     *  If no $content is provided the contentPreview of $source is returned.
     *
     * @param ContentOwner $content
     * @param int $maxLength
     * @return string|null
     * @throws Exception
     */
    public function getContentPreview(ContentOwner $content = null, $maxLength = 60)
    {
        if (!$this->hasContent() && !$content) {
            return null;
        }

        if (!$content) {
            $content = $this->source;
        }

        return RichTextToShortTextConverter::process($content->getContentDescription(), [
            RichTextToShortTextConverter::OPTION_MAX_LENGTH => $maxLength,
            RichTextToShortTextConverter::OPTION_CACHE_KEY => RichTextToShortTextConverter::buildCacheKeyForContent($content),
        ]);
    }

    /**
     * Builds plain text info text about a given content
     *
     * This is a combination of the type of the content with a short preview
     * of it.
     *
     * Note: This should only be used for mail subjects and other plain text
     *
     * If no $content is provided the contentInfo of $source is returned.
     *
     * @param Content $content
     * @return string|null
     * @throws Exception
     * @since 1.4
     */
    public function getContentPlainTextInfo(ContentOwner $content = null, $withContentName = true)
    {
        if (!$this->hasContent() && !$content) {
            return null;
        }

        if (!$content) {
            $content = $this->source;
        }

        $info = $this->getContentPlainTextPreview($content);

        return ($withContentName) ? $content->getContentName() . ' "' . $info . '"' : $info;
    }

    /**
     * Returns a short preview text of the content in plain text. The max length can be defined by setting
     * $maxLength (60 by default).
     *
     *  If no $content is provided the contentPreview of $source is returned.
     *
     * Note: This should only be used for mail subjects and other plain text
     *
     * @param ContentOwner $content
     * @param int $maxLength
     * @return string|null
     * @throws Exception
     * @since 1.4
     */
    public function getContentPlainTextPreview(ContentOwner $content = null, $maxLength = 60)
    {
        if (!$this->hasContent() && !$content) {
            return null;
        }

        if (!$content) {
            $content = $this->source;
        }

        try {
            return RichTextToPlainTextConverter::process($content->getContentDescription(), [
                RichTextToPlainTextConverter::OPTION_MAX_LENGTH => $maxLength,
                RichTextToPlainTextConverter::OPTION_CACHE_KEY => RichTextToPlainTextConverter::buildCacheKeyForContent($content),
            ]);
        } catch (\Exception $e) {
            Yii::error($e);
        }

        return '';
    }


    /**
     * Returns the content name of $content or if not $content is provided of the
     * notification source.
     *
     * @param ContentOwner $content
     * @return string|null
     */
    public function getContentName(ContentOwner $content = null)
    {
        if (!$this->hasContent() && !$content) {
            return null;
        }

        if (!$content) {
            $content = $this->source;
        }

        return $content->getContentName();
    }

    /**
     * Validates the existence of required attributes
     *
     * @return bool
     */
    public function validate()
    {
        if (empty($this->source) && $this->requireSource) {
            return false;
        }

        if (empty($this->originator) && $this->requireOriginator) {
            return false;
        }

        return true;
    }

    /**
     * Serializes the $source and $originator fields.
     *
     * @return array
     * @link http://php.net/manual/en/function.serialize.php
     * @since 1.2
     * @see ActiveRecord::serialize() for the serialization of your $source
     */
    public function __serialize(): array
    {
        $sourceClass = null;
        $sourcePk = null;

        if ($this->source) {
            $sourceClass = get_class($this->source);
            $sourcePk = $this->source->getPrimaryKey();
        }

        $originatorId = ($this->originator != null) ? $this->originator->id : null;

        return [
            'sourceClass' => $sourceClass,
            'sourcePk' => $sourcePk,
            'originator_id' => $originatorId,
        ];
    }

    /**
     * Unserializes the given string, calls the init() function and sets the $source and $originator fields (and $record indirectyl).
     *
     * @param array $serialized
     * @link http://php.net/manual/en/function.unserialize.php
     * @see ActiveRecord::unserialize() for the serialization of your $source
     */
    public function __unserialize($unserializedArr)
    {
        $this->init();

        if (isset($unserializedArr['originator_id'])) {
            $user = User::findOne(['id' => $unserializedArr['originator_id']]);
            if ($user !== null) {
                $this->from($user);
            }
        }

        // Temporary for 1.3.0-beta.2 to 1.3.0-beta.3 updates with existing queue record
        if (isset($unserializedArr['source'])) {
            $this->about($unserializedArr['source']);
        }

        if (!empty($unserializedArr['sourceClass']) && !empty($unserializedArr['sourcePk'])) {
            $source = PolymorphicRelation::loadActiveRecord($unserializedArr['sourceClass'], $unserializedArr['sourcePk']);
            if ($source !== null) {
                $this->about($source);
            }
        }

    }
}
