<?php

namespace Kirby\Cms;

use Closure;
use Kirby\Exception\InvalidArgumentException;
use Kirby\Exception\LogicException;
use Kirby\Filesystem\F;
use Kirby\Form\Form;

/**
 * FileActions
 *
 * @package   Kirby Cms
 * @author    Bastian Allgeier <bastian@getkirby.com>
 * @link      https://getkirby.com
 * @copyright Bastian Allgeier
 * @license   https://getkirby.com/license
 */
trait FileActions
{
    /**
     * Renames the file without touching the extension
     * The store is used to actually execute this.
     *
     * @param string $name
     * @param bool $sanitize
     * @return $this|static
     * @throws \Kirby\Exception\LogicException
     */
    public function changeName(string $name, bool $sanitize = true)
    {
        if ($sanitize === true) {
            $name = F::safeName($name);
        }

        // don't rename if not necessary
        if ($name === $this->name()) {
            return $this;
        }

        return $this->commit('changeName', ['file' => $this, 'name' => $name], function ($oldFile, $name) {
            $newFile = $oldFile->clone([
                'filename' => $name . '.' . $oldFile->extension(),
            ]);

            if ($oldFile->exists() === false) {
                return $newFile;
            }

            if ($newFile->exists() === true) {
                throw new LogicException('The new file exists and cannot be overwritten');
            }

            // remove the lock of the old file
            if ($lock = $oldFile->lock()) {
                $lock->remove();
            }

            // remove all public versions
            $oldFile->unpublish();

            // rename the main file
            F::move($oldFile->root(), $newFile->root());

            if ($newFile->kirby()->multilang() === true) {
                foreach ($newFile->translations() as $translation) {
                    $translationCode = $translation->code();

                    // rename the content file
                    F::move($oldFile->contentFile($translationCode), $newFile->contentFile($translationCode));
                }
            } else {
                // rename the content file
                F::move($oldFile->contentFile(), $newFile->contentFile());
            }

            $newFile->parent()->files()->remove($oldFile->id());
            $newFile->parent()->files()->set($newFile->id(), $newFile);

            return $newFile;
        });
    }

    /**
     * Changes the file's sorting number in the meta file
     *
     * @param int $sort
     * @return static
     */
    public function changeSort(int $sort)
    {
        return $this->commit(
            'changeSort',
            ['file' => $this, 'position' => $sort],
            fn ($file, $sort) => $file->save(['sort' => $sort])
        );
    }

    /**
     * Commits a file action, by following these steps
     *
     * 1. checks the action rules
     * 2. sends the before hook
     * 3. commits the store action
     * 4. sends the after hook
     * 5. returns the result
     *
     * @param string $action
     * @param array $arguments
     * @param Closure $callback
     * @return mixed
     */
    protected function commit(string $action, array $arguments, Closure $callback)
    {
        $old            = $this->hardcopy();
        $kirby          = $this->kirby();
        $argumentValues = array_values($arguments);

        $this->rules()->$action(...$argumentValues);
        $kirby->trigger('file.' . $action . ':before', $arguments);

        $result = $callback(...$argumentValues);

        if ($action === 'create') {
            $argumentsAfter = ['file' => $result];
        } elseif ($action === 'delete') {
            $argumentsAfter = ['status' => $result, 'file' => $old];
        } else {
            $argumentsAfter = ['newFile' => $result, 'oldFile' => $old];
        }
        $kirby->trigger('file.' . $action . ':after', $argumentsAfter);

        $kirby->cache('pages')->flush();
        return $result;
    }

    /**
     * Copy the file to the given page
     *
     * @param \Kirby\Cms\Page $page
     * @return \Kirby\Cms\File
     */
    public function copy(Page $page)
    {
        F::copy($this->root(), $page->root() . '/' . $this->filename());

        if ($this->kirby()->multilang() === true) {
            foreach ($this->kirby()->languages() as $language) {
                $contentFile = $this->contentFile($language->code());
                F::copy($contentFile, $page->root() . '/' . basename($contentFile));
            }
        } else {
            $contentFile = $this->contentFile();
            F::copy($contentFile, $page->root() . '/' . basename($contentFile));
        }

        return $page->clone()->file($this->filename());
    }

    /**
     * Creates a new file on disk and returns the
     * File object. The store is used to handle file
     * writing, so it can be replaced by any other
     * way of generating files.
     *
     * @param array $props
     * @return static
     * @throws \Kirby\Exception\InvalidArgumentException
     * @throws \Kirby\Exception\LogicException
     */
    public static function create(array $props)
    {
        if (isset($props['source'], $props['parent']) === false) {
            throw new InvalidArgumentException('Please provide the "source" and "parent" props for the File');
        }

        // prefer the filename from the props
        $props['filename'] = F::safeName($props['filename'] ?? basename($props['source']));

        $props['model'] = strtolower($props['template'] ?? 'default');

        // create the basic file and a test upload object
        $file = static::factory($props);
        $upload = $file->asset($props['source']);

        // create a form for the file
        $form = Form::for($file, [
            'values' => $props['content'] ?? []
        ]);

        // inject the content
        $file = $file->clone(['content' => $form->strings(true)]);

        // run the hook
        return $file->commit('create', compact('file', 'upload'), function ($file, $upload) {

            // delete all public versions
            $file->unpublish();

            // overwrite the original
            if (F::copy($upload->root(), $file->root(), true) !== true) {
                throw new LogicException('The file could not be created');
            }

            // always create pages in the default language
            if ($file->kirby()->multilang() === true) {
                $languageCode = $file->kirby()->defaultLanguage()->code();
            } else {
                $languageCode = null;
            }

            // store the content if necessary
            $file->save($file->content()->toArray(), $languageCode);

            // add the file to the list of siblings
            $file->siblings()->append($file->id(), $file);

            // return a fresh clone
            return $file->clone();
        });
    }

    /**
     * Deletes the file. The store is used to
     * manipulate the filesystem or whatever you prefer.
     *
     * @return bool
     */
    public function delete(): bool
    {
        return $this->commit('delete', ['file' => $this], function ($file) {

            // remove all versions in the media folder
            $file->unpublish();

            // remove the lock of the old file
            if ($lock = $file->lock()) {
                $lock->remove();
            }

            if ($file->kirby()->multilang() === true) {
                foreach ($file->translations() as $translation) {
                    F::remove($file->contentFile($translation->code()));
                }
            } else {
                F::remove($file->contentFile());
            }

            F::remove($file->root());

            // remove the file from the sibling collection
            $file->parent()->files()->remove($file);

            return true;
        });
    }

    /**
     * Move the file to the public media folder
     * if it's not already there.
     *
     * @return $this
     */
    public function publish()
    {
        Media::publish($this, $this->mediaRoot());
        return $this;
    }

    /**
     * Replaces the file. The source must
     * be an absolute path to a file or a Url.
     * The store handles the replacement so it
     * finally decides what it will support as
     * source.
     *
     * @param string $source
     * @return static
     * @throws \Kirby\Exception\LogicException
     */
    public function replace(string $source)
    {
        $file = $this->clone();

        $arguments = [
            'file' => $file,
            'upload' => $file->asset($source)
        ];

        return $this->commit('replace', $arguments, function ($file, $upload) {

            // delete all public versions
            $file->unpublish();

            // overwrite the original
            if (F::copy($upload->root(), $file->root(), true) !== true) {
                throw new LogicException('The file could not be created');
            }

            // return a fresh clone
            return $file->clone();
        });
    }

    /**
     * Remove all public versions of this file
     *
     * @return $this
     */
    public function unpublish()
    {
        Media::unpublish($this->parent()->mediaRoot(), $this);
        return $this;
    }
}
