<?php

/**
 * @package     Joomla.Administrator
 * @subpackage  com_media
 *
 * @copyright   (C) 2017 Open Source Matters, Inc. <https://www.joomla.org>
 * @license     GNU General Public License version 2 or later; see LICENSE.txt
 */

namespace Joomla\Component\Media\Administrator\Model;

use Joomla\CMS\Component\ComponentHelper;
use Joomla\CMS\Factory;
use Joomla\CMS\Filesystem\File;
use Joomla\CMS\Language\Text;
use Joomla\CMS\MVC\Model\BaseDatabaseModel;
use Joomla\CMS\Object\CMSObject;
use Joomla\CMS\Plugin\PluginHelper;
use Joomla\Component\Media\Administrator\Event\FetchMediaItemEvent;
use Joomla\Component\Media\Administrator\Event\FetchMediaItemsEvent;
use Joomla\Component\Media\Administrator\Event\FetchMediaItemUrlEvent;
use Joomla\Component\Media\Administrator\Exception\FileExistsException;
use Joomla\Component\Media\Administrator\Exception\FileNotFoundException;
use Joomla\Component\Media\Administrator\Exception\InvalidPathException;
use Joomla\Component\Media\Administrator\Provider\ProviderManagerHelperTrait;

// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects

/**
 * Api Model
 *
 * @since  4.0.0
 */
class ApiModel extends BaseDatabaseModel
{
    use ProviderManagerHelperTrait;

    /**
     * The available extensions.
     *
     * @var   string[]
     * @since  4.0.0
     */
    private $allowedExtensions = null;

    /**
     * Returns the requested file or folder information. More information
     * can be found in AdapterInterface::getFile().
     *
     * @param   string  $adapter  The adapter
     * @param   string  $path     The path to the file or folder
     * @param   array   $options  The options
     *
     * @return  \stdClass
     *
     * @since   4.0.0
     * @throws  \Exception
     * @see     AdapterInterface::getFile()
     */
    public function getFile($adapter, $path = '/', $options = [])
    {
        // Add adapter prefix to the file returned
        $file = $this->getAdapter($adapter)->getFile($path);

        // Check if it is a media file
        if ($file->type == 'file' && !$this->isMediaFile($file->path)) {
            throw new InvalidPathException();
        }

        if (isset($options['url']) && $options['url'] && $file->type == 'file') {
            $file->url = $this->getUrl($adapter, $file->path);
        }

        if (isset($options['content']) && $options['content'] && $file->type == 'file') {
            $resource = $this->getAdapter($adapter)->getResource($file->path);

            if ($resource) {
                $file->content = base64_encode(stream_get_contents($resource));
            }
        }

        $file->path    = $adapter . ":" . $file->path;
        $file->adapter = $adapter;

        $event = new FetchMediaItemEvent('onFetchMediaItem', ['item' => $file]);
        Factory::getApplication()->getDispatcher()->dispatch($event->getName(), $event);

        return $event->getArgument('item');
    }

    /**
     * Returns the folders and files for the given path. More information
     * can be found in AdapterInterface::getFiles().
     *
     * @param   string  $adapter  The adapter
     * @param   string  $path     The folder
     * @param   array   $options  The options
     *
     * @return  \stdClass[]
     *
     * @since   4.0.0
     * @throws  \Exception
     * @see     AdapterInterface::getFile()
     */
    public function getFiles($adapter, $path = '/', $options = [])
    {
        // Check whether user searching
        if ($options['search'] != null) {
            // Do search
            $files = $this->search($adapter, $options['search'], $path, $options['recursive']);
        } else {
            // Grab files for the path
            $files = $this->getAdapter($adapter)->getFiles($path);
        }

        // Add adapter prefix to all the files to be returned
        foreach ($files as $key => $file) {
            // Check if the file is valid
            if ($file->type == 'file' && !$this->isMediaFile($file->path)) {
                // Remove the file from the data
                unset($files[$key]);
                continue;
            }

            // Check if we need more information
            if (isset($options['url']) && $options['url'] && $file->type == 'file') {
                $file->url = $this->getUrl($adapter, $file->path);
            }

            if (isset($options['content']) && $options['content'] && $file->type == 'file') {
                $resource = $this->getAdapter($adapter)->getResource($file->path);

                if ($resource) {
                    $file->content = base64_encode(stream_get_contents($resource));
                }
            }

            $file->path    = $adapter . ":" . $file->path;
            $file->adapter = $adapter;
        }

        // Make proper indexes
        $files = array_values($files);

        $event = new FetchMediaItemsEvent('onFetchMediaItems', ['items' => $files]);
        Factory::getApplication()->getDispatcher()->dispatch($event->getName(), $event);

        return $event->getArgument('items');
    }

    /**
     * Creates a folder with the given name in the given path. More information
     * can be found in AdapterInterface::createFolder().
     *
     * @param   string   $adapter   The adapter
     * @param   string   $name      The name
     * @param   string   $path      The folder
     * @param   boolean  $override  Should the folder being overridden when it exists
     *
     * @return  string
     *
     * @since   4.0.0
     * @throws  \Exception
     * @see     AdapterInterface::createFolder()
     */
    public function createFolder($adapter, $name, $path, $override)
    {
        try {
            $file = $this->getFile($adapter, $path . '/' . $name);
        } catch (FileNotFoundException $e) {
            // Do nothing
        }

        // Check if the file exists
        if (isset($file) && !$override) {
            throw new FileExistsException();
        }

        $app               = Factory::getApplication();
        $object            = new CMSObject();
        $object->adapter   = $adapter;
        $object->name      = $name;
        $object->path      = $path;

        PluginHelper::importPlugin('content');

        $result = $app->triggerEvent('onContentBeforeSave', ['com_media.folder', $object, true, $object]);

        if (\in_array(false, $result, true)) {
            throw new \Exception($object->getError());
        }

        $object->name = $this->getAdapter($object->adapter)->createFolder($object->name, $object->path);

        $app->triggerEvent('onContentAfterSave', ['com_media.folder', $object, true, $object]);

        return $object->name;
    }

    /**
     * Creates a file with the given name in the given path with the data. More information
     * can be found in AdapterInterface::createFile().
     *
     * @param   string   $adapter   The adapter
     * @param   string   $name      The name
     * @param   string   $path      The folder
     * @param   string   $data      The data
     * @param   boolean  $override  Should the file being overridden when it exists
     *
     * @return  string
     *
     * @since   4.0.0
     * @throws  \Exception
     * @see     AdapterInterface::createFile()
     */
    public function createFile($adapter, $name, $path, $data, $override)
    {
        try {
            $file = $this->getFile($adapter, $path . '/' . $name);
        } catch (FileNotFoundException $e) {
            // Do nothing
        }

        // Check if the file exists
        if (isset($file) && !$override) {
            throw new FileExistsException();
        }

        // Check if it is a media file
        if (!$this->isMediaFile($path . '/' . $name)) {
            throw new InvalidPathException(Text::_('JLIB_MEDIA_ERROR_WARNFILETYPE'));
        }

        $app               = Factory::getApplication();
        $object            = new CMSObject();
        $object->adapter   = $adapter;
        $object->name      = $name;
        $object->path      = $path;
        $object->data      = $data;
        $object->extension = strtolower(File::getExt($name));

        PluginHelper::importPlugin('content');

        // Also include the filesystem plugins, perhaps they support batch processing too
        PluginHelper::importPlugin('media-action');

        $result = $app->triggerEvent('onContentBeforeSave', ['com_media.file', $object, true, $object]);

        if (\in_array(false, $result, true)) {
            throw new \Exception($object->getError());
        }

        $object->name = $this->getAdapter($object->adapter)->createFile($object->name, $object->path, $object->data);

        $app->triggerEvent('onContentAfterSave', ['com_media.file', $object, true, $object]);

        return $object->name;
    }

    /**
     * Updates the file with the given name in the given path with the data. More information
     * can be found in AdapterInterface::updateFile().
     *
     * @param   string  $adapter  The adapter
     * @param   string  $name     The name
     * @param   string  $path     The folder
     * @param   string  $data     The data
     *
     * @return  void
     *
     * @since   4.0.0
     * @throws  \Exception
     * @see     AdapterInterface::updateFile()
     */
    public function updateFile($adapter, $name, $path, $data)
    {
        // Check if it is a media file
        if (!$this->isMediaFile($path . '/' . $name)) {
            throw new InvalidPathException();
        }

        $app               = Factory::getApplication();
        $object            = new CMSObject();
        $object->adapter   = $adapter;
        $object->name      = $name;
        $object->path      = $path;
        $object->data      = $data;
        $object->extension = strtolower(File::getExt($name));

        PluginHelper::importPlugin('content');

        // Also include the filesystem plugins, perhaps they support batch processing too
        PluginHelper::importPlugin('media-action');

        $result = $app->triggerEvent('onContentBeforeSave', ['com_media.file', $object, false, $object]);

        if (\in_array(false, $result, true)) {
            throw new \Exception($object->getError());
        }

        $this->getAdapter($object->adapter)->updateFile($object->name, $object->path, $object->data);

        $app->triggerEvent('onContentAfterSave', ['com_media.file', $object, false, $object]);
    }

    /**
     * Deletes the folder or file of the given path. More information
     * can be found in AdapterInterface::delete().
     *
     * @param   string  $adapter  The adapter
     * @param   string  $path     The path to the file or folder
     *
     * @return  void
     *
     * @since   4.0.0
     * @throws  \Exception
     * @see     AdapterInterface::delete()
     */
    public function delete($adapter, $path)
    {
        $file = $this->getFile($adapter, $path);

        // Check if it is a media file
        if ($file->type == 'file' && !$this->isMediaFile($file->path)) {
            throw new InvalidPathException();
        }

        $type              = $file->type === 'file' ? 'file' : 'folder';
        $app               = Factory::getApplication();
        $object            = new CMSObject();
        $object->adapter   = $adapter;
        $object->path      = $path;

        PluginHelper::importPlugin('content');

        // Also include the filesystem plugins, perhaps they support batch processing too
        PluginHelper::importPlugin('media-action');

        $result = $app->triggerEvent('onContentBeforeDelete', ['com_media.' . $type, $object]);

        if (\in_array(false, $result, true)) {
            throw new \Exception($object->getError());
        }

        $this->getAdapter($object->adapter)->delete($object->path);

        $app->triggerEvent('onContentAfterDelete', ['com_media.' . $type, $object]);
    }

    /**
     * Copies file or folder from source path to destination path
     * If forced, existing files/folders would be overwritten
     *
     * @param   string  $adapter          The adapter
     * @param   string  $sourcePath       Source path of the file or folder (relative)
     * @param   string  $destinationPath  Destination path(relative)
     * @param   bool    $force            Force to overwrite
     *
     * @return  string
     *
     * @since   4.0.0
     * @throws  \Exception
     */
    public function copy($adapter, $sourcePath, $destinationPath, $force = false)
    {
        return $this->getAdapter($adapter)->copy($sourcePath, $destinationPath, $force);
    }

    /**
     * Moves file or folder from source path to destination path
     * If forced, existing files/folders would be overwritten
     *
     * @param   string  $adapter          The adapter
     * @param   string  $sourcePath       Source path of the file or folder (relative)
     * @param   string  $destinationPath  Destination path(relative)
     * @param   bool    $force            Force to overwrite
     *
     * @return  string
     *
     * @since   4.0.0
     * @throws  \Exception
     */
    public function move($adapter, $sourcePath, $destinationPath, $force = false)
    {
        return $this->getAdapter($adapter)->move($sourcePath, $destinationPath, $force);
    }

    /**
     * Returns a url for serve media files from adapter.
     * Url must provide a valid image type to be displayed on Joomla! site.
     *
     * @param   string  $adapter  The adapter
     * @param   string  $path     The relative path for the file
     *
     * @return  string  Permalink to the relative file
     *
     * @since   4.0.0
     * @throws  FileNotFoundException
     */
    public function getUrl($adapter, $path)
    {
        // Check if it is a media file
        if (!$this->isMediaFile($path)) {
            throw new InvalidPathException();
        }

        $url = $this->getAdapter($adapter)->getUrl($path);

        $event = new FetchMediaItemUrlEvent('onFetchMediaFileUrl', ['adapter' => $adapter, 'path' => $path, 'url' => $url]);
        Factory::getApplication()->getDispatcher()->dispatch($event->getName(), $event);

        return $event->getArgument('url');
    }

    /**
     * Search for a pattern in a given path
     *
     * @param   string  $adapter    The adapter to work on
     * @param   string  $needle     The search therm
     * @param   string  $path       The base path for the search
     * @param   bool    $recursive  Do a recursive search
     *
     * @return \stdClass[]
     *
     * @since   4.0.0
     * @throws \Exception
     */
    public function search($adapter, $needle, $path = '/', $recursive = true)
    {
        return $this->getAdapter($adapter)->search($path, $needle, $recursive);
    }

    /**
     * Checks if the given path is an allowed media file.
     *
     * @param   string  $path  The path to file
     *
     * @return boolean
     *
     * @since   4.0.0
     */
    private function isMediaFile($path)
    {
        // Check if there is an extension available
        if (!strrpos($path, '.')) {
            return false;
        }

        // Initialize the allowed extensions
        if ($this->allowedExtensions === null) {
            // Get options from the input or fallback to images only
            $mediaTypes = explode(',', Factory::getApplication()->getInput()->getString('mediatypes', '0'));
            $types      = [];
            $extensions = [];

            // Default to showing all supported formats
            if (\count($mediaTypes) === 0) {
                $mediaTypes = ['0', '1', '2', '3'];
            }

            array_map(
                function ($mediaType) use (&$types) {
                    switch ($mediaType) {
                        case '0':
                            $types[] = 'images';
                            break;
                        case '1':
                            $types[] = 'audios';
                            break;
                        case '2':
                            $types[] = 'videos';
                            break;
                        case '3':
                            $types[] = 'documents';
                            break;
                        default:
                            break;
                    }
                },
                $mediaTypes
            );

            $images = array_map(
                'trim',
                explode(
                    ',',
                    ComponentHelper::getParams('com_media')->get(
                        'image_extensions',
                        'bmp,gif,jpg,jpeg,png,webp,avif'
                    )
                )
            );
            $audios = array_map(
                'trim',
                explode(
                    ',',
                    ComponentHelper::getParams('com_media')->get(
                        'audio_extensions',
                        'mp3,m4a,mp4a,ogg'
                    )
                )
            );
            $videos = array_map(
                'trim',
                explode(
                    ',',
                    ComponentHelper::getParams('com_media')->get(
                        'video_extensions',
                        'mp4,mp4v,mpeg,mov,webm'
                    )
                )
            );
            $documents = array_map(
                'trim',
                explode(
                    ',',
                    ComponentHelper::getParams('com_media')->get(
                        'doc_extensions',
                        'doc,odg,odp,ods,odt,pdf,ppt,txt,xcf,xls,csv'
                    )
                )
            );

            foreach ($types as $type) {
                if (\in_array($type, ['images', 'audios', 'videos', 'documents'])) {
                    $extensions = array_merge($extensions, ${$type});
                }
            }

            // Make them an array
            $this->allowedExtensions = $extensions;
        }

        // Extract the extension
        $extension = strtolower(substr($path, strrpos($path, '.') + 1));

        // Check if the extension exists in the allowed extensions
        return \in_array($extension, $this->allowedExtensions);
    }
}
