<?php

namespace humhub\modules\cfiles\models;

use humhub\modules\content\components\ContentContainerActiveRecord;
use humhub\modules\content\models\Content;
use humhub\modules\file\libs\ImageHelper;
use humhub\modules\file\models\FileContent;
use humhub\modules\file\libs\FileHelper;
use humhub\modules\user\models\User;
use humhub\modules\search\events\SearchAddEvent;
use humhub\modules\space\models\Space;
use Yii;
use yii\db\ActiveQuery;
use yii\imagine\Image;
use yii\web\UploadedFile;

/**
 * This is the model class for table "cfiles_folder".
 *
 * @property int $id
 * @property int $parent_folder_id
 * @property string $title
 * @property string $description
 * @property string $type
 *
 * @property Folder parentFolder
 * @property Folder[] folders
 * @property Folder[] subFolders
 * @property Folder[] specialFolders
 * @property File[] files
 * @property File[] subFiles
 *
 */
class Folder extends FileSystemItem
{
    public const TYPE_FOLDER_ROOT = 'root';
    public const TYPE_FOLDER_POSTED = 'posted';
    public const ROOT_TITLE = 'Root';
    public const ROOT_DESCRIPTION = 'The root folder is the entry point that contains all available files.';
    public const ALL_POSTED_FILES_TITLE = 'Files from the stream';
    public const ALL_POSTED_FILES_DESCRIPTION = 'You can find all files that have been posted to this stream here.';

    /**
     * @inheritdoc
     */
    public $wallEntryClass = "humhub\modules\cfiles\widgets\WallEntryFolder";

    /**
     * @inheritdoc
     */
    public $streamChannel = null;

    /**
     * @inheritdoc
     */
    public static function tableName()
    {
        return 'cfiles_folder';
    }

    /**
     * @inheritdoc
     */
    public function getContentName()
    {
        return Yii::t('CfilesModule.base', "Folder");
    }

    /**
     * @inheritdoc
     */
    public function getIcon()
    {
        return'fa-folder';
    }

    /**
     * @inheritdoc
     */
    public function rules()
    {

        $result = array_merge(parent::rules(), [
            ['parent_folder_id', 'integer'],
            ['parent_folder_id', 'validateParentFolderId'],
            ['title', 'required'],
            ['title', 'trim'],
            ['title', 'string', 'min' => 1, 'max' => 255],
            ['title', 'noSpaces'],
            ['description', 'string', 'max' => 255],
            ['title', 'uniqueTitle'],
        ]);

        if (!$this->isRoot()) {
            $result[] = ['parent_folder_id', 'required'];
        }

        return $result;
    }

    /**
     * Makes sure that after an title change there is no equal title for the given container in the given parent folder.
     *
     * @param string $attribute
     * @param array $params
     * @param string $validator
     */
    public function uniqueTitle($attribute, $params, $validator)
    {
        if ($this->isRoot() || !$this->hasTitleChanged()) {
            return;
        }

        if ($this->parentFolder->folderExists($this->title)) {
            $this->addError('title', \Yii::t('CfilesModule.base', 'A folder with this name already exists.'));
        }
    }

    /**
     * @inheritdoc
     */
    public function attributeLabels()
    {
        return array_merge(parent::attributeLabels(), [
            'id' => 'ID',
            'parent_folder_id' => Yii::t('CfilesModule.base', 'Parent Folder ID'),
            'title' => Yii::t('CfilesModule.base', 'Title'),
            'description' => Yii::t('CfilesModule.base', 'Description'),
        ]);
    }

    /**
     * @inheritdoc
     */
    public function attributeHints()
    {
        if (!$this->isNewRecord) {
            return ['visibility' => Yii::t('CfilesModule.base', 'Note: Changes of the folders visibility, will be inherited by all contained files and folders.')];
        }
        return parent::attributeHints();
    }

    /**
     * @inheritdoc
     */
    public function getSearchAttributes()
    {
        if ($this->isAllPostedFiles() || $this->isRoot()) {
            $attributes = [];
        } else {
            $attributes = [
                'name' => $this->title,
                'description' => $this->description,
            ];

            if ($this->getCreator()) {
                $attributes['creator'] = $this->getCreator()->getDisplayName();
            }

            if ($this->getEditor()) {
                $attributes['editor'] = $this->getEditor()->getDisplayName();
            }
        }
        $this->trigger(self::EVENT_SEARCH_ADD, new SearchAddEvent($attributes));
        return $attributes;
    }

    /**
     * @inheritdoc
     */
    public function beforeSave($insert)
    {
        if ($insert && $this->visibility !== null) {
            $this->content->visibility = $this->visibility;
        } elseif ($this->visibility !== null && $this->visibility != $this->content->visibility) {
            $this->updateVisibility($this->visibility);
        }
        return parent::beforeSave($insert);
    }

    /**
     * @inheritdoc
     */
    public function afterMove(?ContentContainerActiveRecord $container = null)
    {
        parent::afterMove($container);

        // Move all sub folders and files into the same Container where this Folder has been moved to
        $this->moveSubFoldersToContainer($container);
        $this->moveSubFilesToContainer($container);
    }

    public function moveSubFoldersToContainer(?ContentContainerActiveRecord $container = null)
    {
        if ($container === null) {
            $container = $this->content->getContainer();
        }

        $folders = Folder::find()
            ->andWhere(['parent_folder_id' => $this->id])
            ->all();

        foreach ($folders as $folder) {
            /* @var Folder $folder */
            $folder->move($container);
        }
    }

    public function moveSubFilesToContainer(?ContentContainerActiveRecord $container = null)
    {
        if ($container === null) {
            $container = $this->content->getContainer();
        }

        $files = File::find()
            ->joinWith('baseFile')
            ->andWhere(['cfiles_file.parent_folder_id' => $this->id])
            ->all();

        foreach ($files as $file) {
            /* @var File $file */
            $file->move($container);
        }
    }

    /**
     * @param $visibility
     */
    public function updateVisibility($visibility)
    {
        if ($visibility === null) {
            return;
        }

        $this->content->visibility = $visibility;

        foreach ($this->getSubFiles() as $file) {
            $file->content->visibility = $visibility;
            $file->content->save();
        }

        foreach ($this->getSubFolders() as $folder) {
            $folder->updateVisibility($visibility);
            $folder->content->save();
        }
    }

    /**
     * @inheritdoc
     */
    public function afterSoftDelete()
    {
        foreach ($this->folders as $folder) {
            $folder->delete();
        }

        foreach ($this->files as $file) {
            $file->delete();
        }

        parent::afterSoftDelete();
    }

    /**
     * @inheritdoc
     */
    public function beforeDelete()
    {
        foreach ($this->folders as $folder) {
            $folder->hardDelete();
        }

        foreach ($this->files as $file) {
            $file->hardDelete();
        }

        return parent::beforeDelete();
    }

    public function getVisibilityTitle()
    {
        if (Yii::$app->getModule('friendship')->settings->get('enable') && $this->content->container instanceof User) {
            if ($this->content->container->isCurrentuser()) {
                $privateText =  Yii::t('CfilesModule.base', 'This folder is only visible for you and your friends.');
            } else {
                $privateText =  Yii::t('CfilesModule.base', 'This folder is protected.');
            }

            return $this->content->isPublic() ? Yii::t('CfilesModule.base', 'This folder is public.') : $privateText;
        }

        return $this->content->isPublic() ? Yii::t('CfilesModule.base', 'This folder is public.') : Yii::t('CfilesModule.base', 'This folder is private.');
    }

    /**
     * In older versions there was no actual root folder, all root files and folders had parent_folder_id 0 or null.
     * This function can be executed for newly created root folders and will move all files/folders to the new root.
     */
    public function migrateFromOldStructure()
    {
        if (!$this->isRoot()) {
            return;
        }

        $filesQuery = File::find()->joinWith('baseFile')->contentContainer($this->content->container)
            ->andWhere(['OR', ['IS', 'cfiles_file.parent_folder_id', new \yii\db\Expression('NULL')], ['cfiles_file.parent_folder_id' => 0]]);

        $foldersQuery = Folder::find()->contentContainer($this->content->container)
            ->andWhere(['OR', ['IS', 'cfiles_folder.parent_folder_id', new \yii\db\Expression('NULL')], ['cfiles_folder.parent_folder_id' => 0]])
            ->andWhere(['IS', 'cfiles_folder.type', new \yii\db\Expression('NULL')]);

        $rootsubfiles = $filesQuery->all();
        $rootsubfolders = $foldersQuery->all();

        foreach ($rootsubfiles as $file) {
            $file->parent_folder_id = $this->id;
            $file->save();
        }

        foreach ($rootsubfolders as $folder) {
            $folder->parent_folder_id = $this->id;
            $folder->save();
        }
    }

    /**
     * Initializes a root folder for the given $contentContainer
     * @param ContentContainerActiveRecord $contentContainer
     * @return Folder|bool
     */
    public static function initRoot(ContentContainerActiveRecord $contentContainer)
    {
        if (!empty(self::getRoot($contentContainer))) {
            return false;
        }

        $root = new self($contentContainer, Content::VISIBILITY_PUBLIC, [
            'type' => self::TYPE_FOLDER_ROOT,
            'title' => self::ROOT_TITLE,
            'description' => self::ROOT_DESCRIPTION,
        ]);

        $root->content->created_by = self::getContainerOwnerId($contentContainer);
        $root->silentContentCreation = true;
        if ($root->save()) {
            return $root;
        }

        return false;
    }

    /**
     * Initializes the posted files folder for the given $contentContainer
     * @param ContentContainerActiveRecord $contentContainer
     * @return bool|Folder
     */
    public static function initPostedFilesFolder(ContentContainerActiveRecord $contentContainer)
    {
        $root = self::getRoot($contentContainer);

        if (!$root || !empty(self::getPostedFilesFolder($contentContainer))) {
            return false;
        }

        $postedFilesFolder = new self($contentContainer, Content::VISIBILITY_PUBLIC, [
            'type' => self::TYPE_FOLDER_POSTED,
            'title' => self::ALL_POSTED_FILES_TITLE,
            'description' => self::ALL_POSTED_FILES_DESCRIPTION,
            'parent_folder_id' => $root->id,
        ]);

        $postedFilesFolder->content->created_by = self::getContainerOwnerId($contentContainer);
        $postedFilesFolder->silentContentCreation = true;
        if ($postedFilesFolder->save()) {
            return $postedFilesFolder;
        }

        return false;
    }

    /**
     * Ensures root folders are owned by the container owner so they survive unrelated user deletions.
     * @return bool whether the owner was changed
     */
    public static function ensureRootFolderOwner(?self $folder, ContentContainerActiveRecord $contentContainer): bool
    {
        if ($folder === null || $folder->content === null) {
            return false;
        }

        $ownerId = self::getContainerOwnerId($contentContainer);
        if ($ownerId === null || $folder->content->created_by === $ownerId) {
            return false;
        }

        $folder->content->created_by = $ownerId;
        return (bool) $folder->content->save(false, ['created_by']);
    }

    /**
     * Ensures the virtual root-folder structure exists and belongs to the container owner.
     */
    public static function ensureRootFolderStructure(ContentContainerActiveRecord $contentContainer): void
    {
        $root = self::getRoot($contentContainer);
        $rootCreated = false;
        if ($root === null) {
            $root = self::initRoot($contentContainer);
            $rootCreated = $root instanceof self;
        }

        if ($root === false) {
            return;
        }

        self::ensureRootFolderOwner($root, $contentContainer);
        if ($rootCreated) {
            $root->migrateFromOldStructure();
        }

        $postedFilesFolder = self::getPostedFilesFolder($contentContainer) ?: self::initPostedFilesFolder($contentContainer);
        if ($postedFilesFolder === false) {
            return;
        }

        self::ensureRootFolderOwner($postedFilesFolder, $contentContainer);
        self::ensurePostedFilesFolderRoot($postedFilesFolder, $root);
    }

    /**
     * Ensures the virtual "Files from the stream" folder stays attached to the current root folder.
     */
    public static function ensurePostedFilesFolderRoot(?self $postedFilesFolder, ?self $rootFolder): bool
    {
        if ($postedFilesFolder === null || $rootFolder === null || (int)$postedFilesFolder->parent_folder_id === (int)$rootFolder->id) {
            return false;
        }

        $postedFilesFolder->parent_folder_id = $rootFolder->id;

        return (bool) $postedFilesFolder->save(false, ['parent_folder_id']);
    }

    /**
     * Generate the maximum depth directory structure originating from a given folder id.
     *
     * @param Folder $parentId
     * @return array [['folder' => --current folder--, 'subfolders' => [['folder' => --current folder--, 'subfolders' => []], ...], ['folder' => --current folder--, 'subfolders' => [['folder' => --current folder--, 'subfolders' => []], ...], ...]
     */
    public static function getFolderList($parent, $orderBy = ['title' => SORT_ASC])
    {
        $parentId = ($parent instanceof Folder) ? $parent->id : $parent;

        $dirStruc = [];
        foreach (self::getSubFoldersByParent($parent, $orderBy)->all() as $folder) {
            $dirStruc[] = ['folder' => $folder, 'subfolders' => self::getFolderlist($folder, $orderBy)];
        }

        return $dirStruc;
    }

    /**
     * Returns all readable subfolders of the given parent folder.
     *
     * @param $contentContainer
     * @param Folder $parent
     * @param array $orderBy
     * @return ActiveQuery
     */
    public static function getSubFoldersByParent($parent, $orderBy = ['title' => SORT_ASC])
    {
        $query = Folder::find()->contentContainer($parent->content->container)->readable();
        $query->andWhere(['cfiles_folder.parent_folder_id' => $parent->id]);

        // do not return any subfolders here that are root or allpostedfiles
        $query->andWhere([
            'or',
            ['cfiles_folder.type' => null],
            ['and',
                ['<>', 'cfiles_folder.type', Folder::TYPE_FOLDER_POSTED],
                ['<>', 'cfiles_folder.type', Folder::TYPE_FOLDER_ROOT],
            ],
        ]);

        return $query->orderBy($orderBy);
    }

    /**
     * @param ContentContainerActiveRecord $contentContainer
     * @return int
     */
    private static function getContainerOwnerId(ContentContainerActiveRecord $contentContainer)
    {
        if ($contentContainer instanceof User) {
            return $contentContainer->id;
        } elseif ($contentContainer instanceof Space) {
            return $contentContainer->created_by;
        }

        return null;
    }

    /**
     * @param ContentContainerActiveRecord $contentContainer
     * @return Folder the root folder of the given ContentContainerActiveRecord
     */
    public static function getOrInitRoot(ContentContainerActiveRecord $contentContainer)
    {
        if ($root = Folder::getRoot($contentContainer)) {
            return $root;
        }

        return Folder::initRoot($contentContainer);
    }

    /**
     * @param ContentContainerActiveRecord $contentContainer
     * @return Folder the root folder of the given ContentContainerActiveRecord
     */
    public static function getRoot(ContentContainerActiveRecord $contentContainer)
    {
        return self::find()->contentContainer($contentContainer)->andWhere(['type' => self::TYPE_FOLDER_ROOT])->one();
    }

    /**
     * @param ContentContainerActiveRecord $contentContainer
     * @return Folder the root folder of the given ContentContainerActiveRecord
     */
    public static function getPostedFilesFolder(ContentContainerActiveRecord $contentContainer)
    {
        return self::find()->contentContainer($contentContainer)->andWhere(['type' => self::TYPE_FOLDER_POSTED])->one();
    }

    /**
     * @return ActiveQuery of all direct child files
     */
    public function getFiles()
    {
        return $this->hasMany(File::className(), ['parent_folder_id' => 'id'])
                        ->joinWith('baseFile')
                        ->orderBy(['title' => SORT_ASC]);
    }

    /**
     * @return ActiveQuery of all direct child folders
     */
    public function getFolders()
    {
        return $this->hasMany(Folder::className(), ['parent_folder_id' => 'id'])->orderBy(['title' => SORT_ASC]);
    }

    /**
     * @return bool
     */
    public function hasTitleChanged()
    {
        return $this->isNewRecord || $this->getOldAttribute('title') != $this->title;
    }

    /**
     * @inheritdoc
     */
    public function getItemId()
    {
        return $this->getItemType() . '_' . $this->id;
    }

    /**
     * @inheritdoc
     */
    public function getContentId()
    {
        return $this->content->id;
    }

    /**
     * @return string
     */
    public function getItemType()
    {
        return 'folder' . ($this->type !== null ? '-' . $this->type : '');
    }

    /**
     * @inheritdoc
     */
    public function getTitle()
    {
        if ($this->isRoot()) {
            return  Yii::t('CfilesModule.base', 'Root');
        } elseif ($this->isAllPostedFiles()) {
            return  Yii::t('CfilesModule.base', 'Files from the stream');
        }

        return $this->title;
    }

    public function getDescription()
    {
        if ($this->isRoot()) {
            return  Yii::t('CfilesModule.base', 'The root folder is the entry point that contains all available files.');
        } elseif ($this->isAllPostedFiles()) {
            return  Yii::t('CfilesModule.base', 'You can find all files that have been posted to this stream here.');
        }

        return $this->description;
    }

    /**
     * @return string
     */
    public function getDownloadCount()
    {
        return '';
    }

    /**
     * @inheritdoc
     */
    public function getSize()
    {
        return 0;
    }

    public function createUrl($route = null, $params = [], $scheme = false)
    {
        $params = (is_array($params)) ? $params : [];
        $params['fid'] = $this->id;
        return $this->content->container->createUrl($route, $params, $scheme);
    }

    /**
     * @inheritdoc
     */
    public function getUrl(bool $scheme = false)
    {
        if (empty($this->content->container)) {
            return '';
        }

        return $this->content->container->createUrl('/cfiles/browse/index', ['fid' => $this->id], $scheme);
    }

    /**
     * @inheritdoc
     */
    public function getEditUrl()
    {
        return $this->content->container->createUrl('/cfiles/edit/folder', ['id' => $this->getItemId()]);
    }

    public function noSpaces($attribute, $params)
    {
        if (trim((string) $this->$attribute) !== $this->$attribute) {
            $this->addError($attribute, Yii::t('CfilesModule.base', 'Folder should not start or end with blank space.'));
        }
    }

    public function getFullPath($separator = '/')
    {
        return static::getPathFromId($this->id, false, $separator);
    }

    public static function getPathFromId($id, $parentFolderPath = false, $separator = '/', $withRoot = false)
    {
        if ($id == 0) {
            return $separator;
        }
        $item = Folder::findOne([
            'id' => $id,
        ]);
        if (empty($item)) {
            return null;
        }
        $tempFolder = $item->parentFolder;
        $path = '';
        if (!$parentFolderPath) {
            if ($item->isRoot()) {
                if ($withRoot) {
                    $path .= $item->title;
                }
            } else {
                $path .= $separator . $item->title;
            }
        }
        $counter = 0;
        // break at maxdepth to avoid hangs
        while (!empty($tempFolder)) {
            if ($tempFolder->isRoot()) {
                if ($withRoot) {
                    $path = $tempFolder->title . $path;
                }
                break;
            } else {
                if (++$counter > 10) {
                    $path = '...' . $path;
                    break;
                }
                $path = $separator . $tempFolder->title . $path;
            }

            $tempFolder = $tempFolder->parentFolder;
        }
        return $path;
    }

    /**
     * Returns the folder path as ordered array.
     * @return Folder[]
     */
    public function getCrumb()
    {
        $parent = $this;
        do {
            $crumb[] = $parent;
            $parent = $parent->parentFolder;
        } while ($parent != null);
        return array_reverse($crumb);
    }

    /**
     * @inheritdoc
     */
    public function getContentDescription()
    {
        return $this->title;
    }

    public function isRoot()
    {
        return $this->type === self::TYPE_FOLDER_ROOT;
    }

    public function isAllPostedFiles()
    {
        return $this->type === self::TYPE_FOLDER_POSTED;
    }

    /**
     * Validate parent folder id
     *
     * @param string $attribute the attribute name
     */
    public function validateParentFolderId($attribute = 'parent_folder_id')
    {
        $parent = $this->parentFolder;

        // check if one of the parents is oneself to avoid circles
        while (!empty($parent)) {
            if ($this->id == $parent->id) {
                $this->addError($attribute, Yii::t('CfilesModule.base', 'Please select a valid destination folder for %title%.', ['%title%' => $this->title]));
                break;
            }
            $parent = static::findOne(['id' => $parent->parent_folder_id]);
        }

        parent::validateParentFolderId($attribute);
    }

    /**
     * @return FileSystemItem[] return all child folders and child files excluding special folders
     */
    public function getChildren()
    {
        return array_merge($this->getSubFolders(), $this->getSubFiles());
    }

    /**
     * @param array $order
     * @return Folder[]
     */
    public function getSpecialFolders()
    {
        $specialFoldersQuery = Folder::find()->contentContainer($this->content->container)->readable();
        $specialFoldersQuery->andWhere(['cfiles_folder.parent_folder_id' => $this->id]);
        $specialFoldersQuery->andWhere(['is not', 'cfiles_folder.type', null]);
        return $specialFoldersQuery->all();
    }

    /**
     * @param string|array $order
     * @return Folder[]
     */
    public function getSubFolders($order = ['title' => SORT_ASC])
    {
        return self::getSubFoldersByParent($this, $order)->all();
    }

    /**
     * @param string|array $order
     * @return File[]
     */
    public function getSubFiles($order = ['file.file_name' => SORT_ASC])
    {
        $filesQuery = File::find()->joinWith('baseFile')->contentContainer($this->content->container)->readable();
        $filesQuery->andWhere(['cfiles_file.parent_folder_id' => $this->id]);
        $filesQuery->orderBy($order);
        return $filesQuery->all();
    }

    /**
     * Creates and adds the given UploadedFile to this directory.
     *
     * Returns the newly created cfiles file.
     * The calling function has to make sure there are no errors by checking_
     *
     * ```php
     * $file->hasErrors()
     * ```
     * and
     *
     * ```php
     * $file->baseFile->hasErrors();
     * ```
     * @param UploadedFile $uploadedFile
     * @return File
     */
    public function addUploadedFile(UploadedFile $uploadedFile): File
    {
        // Get file instance either an existing one or a new one
        $file = $this->getFileInstance($uploadedFile);

        if ($file->setUploadedFile($uploadedFile)) {
            $file->save();
        }

        return $file;
    }

    /**
     * @param UploadedFile $uploadedFile
     * @return File
     */
    private function getFileInstance(UploadedFile $uploadedFile): File
    {
        if ($file = $this->findFileByName($uploadedFile->name)) {
            if ($file->content->getStateService()->isPublished()) {
                // Allow to use only Published file with same name
                return $file;
            }
            // otherwise old not published file must be renamed in order to avoid conflicts
            $file->renameConflicted();
        }

        return new File($this->content->container, $this->getNewItemVisibility(), [
            'parent_folder_id' => $this->id,
        ]);
    }

    private function getNewItemVisibility()
    {
        if ($this->isRoot()) {
            return $this->content->container->getDefaultContentVisibility();
        }

        return $this->content->visibility;
    }

    public function addFileFromPath($filename, $filePath)
    {
        $file = new File($this->content->container, $this->getNewItemVisibility(), [
            'parent_folder_id' => $this->id,
        ]);

        $fileContent = new FileContent([
            'mime_type' => FileHelper::getMimeType($filePath),
            'size' => filesize($filePath),
            'show_in_stream' => 0,
            'file_name' => $this->getAddedFileName($filename),
        ]);

        if ($fileContent->mime_type == 'image/jpeg') {
            $image = Image::getImagine()->open($filePath);
            ImageHelper::fixJpegOrientation($image, $filePath);
        }

        $fileContent->newFileContent = stream_get_contents(fopen($filePath, 'r'));

        $file->setFileContent($fileContent);
        $file->save();

        return $file;
    }

    /**
     * Creates a new non persisted folder within this folder.
     *
     * @param string|null $title
     * @param string|null $description
     * @return Folder
     */
    public function newFolder($title = null, $description = null)
    {
        return new self($this->content->container, $this->getNewItemVisibility(), [
            'parent_folder_id' => $this->id,
            'title' => $title,
            'description' => $description]);
    }

    /**
     * Moves the given item into this folder.
     *
     * This method checks for duplicate file/folder names.
     *
     * If a file with the same title already exists we use a file name index e.g. file(1).txt
     *
     * If a folder already exists with the same title we merge all sub items into the existing folder
     *
     *
     * @param FileSystemItem $item
     * @return bool
     */
    public function moveItem(FileSystemItem $item)
    {
        if (!$item) {
            return false;
        }

        if (!$item->canManage()) {
            if ($item instanceof File) {
                $item->addError($item->getTitle(), Yii::t('CfilesModule.base', 'You cannot move the file "{name}"!', ['name' => $item->getTitle()]));
            } else {
                $item->addError($item->getTitle(), Yii::t('CfilesModule.base', 'You cannot move the folder "{name}"!', ['name' => $item->getTitle()]));
            }
            return false;
        }

        if ($item instanceof Folder && !$item->isEditableFolder()) {
            $item->addError($item->getTitle(), Yii::t('CfilesModule.base', 'Folder {name} given folder is not editable!', ['name' => $item->getTitle()]));
            return false;
        }

        if ($item->getItemId() === $this->getItemId()) {
            $item->addError($item->getTitle(), Yii::t('CfilesModule.base', 'Folder {name} can\'t be moved to itself!', ['name' => $item->getTitle()]));
            return false;
        }

        // We ignore invalid items and items already residing in the given destination
        if ($item->hasParent($this) || $item->is($this)) {
            return true;
        }

        // Note we don't set the content visibility directly to run recursive visibility change in folders
        $item->visibility = $this->content->visibility;
        $item->parent_folder_id = $this->id;

        $moveResult = $this->checkForDuplicate($item);

        if (!$moveResult) {
            // Probably an error when moving subfiles to an existing folder
            return false;
        }

        if ($item->is($moveResult)) {
            // Either no duplicate or just simple file rename
            return $moveResult->save();
        }

        // Successfully moved subfiles to existing folder with same title
        return true;
    }

    /**
     * Checks if the given $item title already exists in this folder and renames already existing files or
     * merges already existing folders.
     *
     * This method returns either the item itself in case there was no duplicate or a file duplicate (which was renamed)
     * or the already existing folder in case the item is a folder with the same title as an existing sub folder,
     * or null in case there was an error when moving files to an existing subfolder.
     *
     * @param FileSystemItem $item
     * @return FileSystemItem|null
     */
    private function checkForDuplicate(FileSystemItem $item)
    {
        $result = null;
        if ($item instanceof File) {
            $item->setTitle($this->getAddedFileName($item->getTitle()));
            $result = $item;
        } elseif ($item instanceof Folder) {
            $result = $item;

            $existingFolderWithTitle = $this->findFolderByName($item->title);

            // Check if the folder exists if not, move children to existing subfolder, if there is an error we set §result to null
            if ($existingFolderWithTitle && !$existingFolderWithTitle->is($item)) {
                $result = $existingFolderWithTitle;
                foreach ($item->getChildren() as $child) {
                    // if moving the given item fails we set result to null and add an item error
                    if (!$existingFolderWithTitle->moveItem($child)) {
                        $result = null;
                        foreach ($child->getErrors() as $errors) {
                            $item->addErrors([$child->getTitle() => $errors]);
                        }
                    };
                }

                if ($result) {
                    $item->delete();
                }
            }
        }

        return $result;
    }

    /**
     * Searches for direct sub files with the given file name and returns an indexed file name in form of
     * myFile(<index>).txt in case an existing file was found, otherwise the original fileName is returned.
     *
     * @param $fileName
     * @return string either an indexed file name or original filename if no duplicate title was found.
     */
    protected function getAddedFileName($fileName)
    {
        $counter = 0;
        $parts = preg_split('~\.(?=[^\.]*$)~', (string) $fileName);
        $origName = $parts[0];
        $ext = count($parts) == 2 ? '.' . $parts[1] : '';

        while ($this->fileExists($fileName)) {
            $fileName = $origName . '(' . ++$counter . ')' . $ext;
        }

        return $fileName;
    }

    public function fileExists($name)
    {
        return File::find()->joinWith('baseFile')->where(['file_name' => $name, 'cfiles_file.parent_folder_id' => $this->id])->count();
    }

    public function folderExists($name)
    {
        return Folder::find()->where(['title' => $name, 'parent_folder_id' => $this->id])->count();
    }

    public function findFileByName($name): ?File
    {
        return File::find()->contentContainer($this->content->container)
            ->joinWith('baseFile')
            ->andWhere(['file_name' => $name])
            ->andWhere(['cfiles_file.parent_folder_id' => $this->id])
            ->one();
    }

    public function findFolderByName($name): ?Folder
    {
        return Folder::find()->contentContainer($this->content->container)
            ->andWhere(['title' => $name, 'parent_folder_id' => $this->id])->one();
    }

    /**
     * @inheritdoc
     */
    public function getVersionsUrl(int $versionId = 0): ?string
    {
        return null;
    }

    /**
     * @inheritdoc
     */
    public function getDeleteVersionUrl(int $versionId): ?string
    {
        return null;
    }

    /**
     * @inheritdoc
     */
    public function renameConflicted(): bool
    {
        if ($this->isNewRecord) {
            return false;
        }

        $this->title = 'conflict' . $this->id . '-' . $this->title;

        return $this->save();
    }

    /**
     * Resolve conflicts before creating new folder
     *
     * @param string|null $title
     */
    public function resolveConflictsBeforeCreate(?string $title = null)
    {
        if ($title === null || $title === '') {
            return;
        }

        $duplicatedFolder = $this->findFolderByName($title);

        if ($duplicatedFolder && !$duplicatedFolder->content->getStateService()->isPublished()) {
            // Rename already existing Folder with same name but not Published(it maybe soft deleted),
            // It is useful to avoid a conflict on creating a new Folder with same name
            $duplicatedFolder->renameConflicted();
        }
    }
}
