<?PHP
#
#   FILE:  FolderFactory.php
#
#   Part of the Metavus digital collections platform
#   Copyright 2012-2025 Edward Almasy and Internet Scout Research Group
#   http://metavus.net
#
# @scout:phpstan

namespace Metavus;
use InvalidArgumentException;
use ScoutLib\ItemFactory;

/**
 * Factory object for Folder class, used to retrieve and manage Folders
 * and groups of Folders.
 */
class FolderFactory extends ItemFactory
{

    # ---- PUBLIC INTERFACE --------------------------------------------------

    /**
     * Constructor for FolderFactory.
     * @param int $OwnerId ID of owner of folders to be manipulated by factory.
     *       If specified then all operations pertain only to folders with
     *       the specified owner.  (OPTIONAL)
     */
    public function __construct(?int $OwnerId = null)
    {
        # set up item factory base class
        parent::__construct("Folder", "Folders", "FolderId", "FolderName", true);

        # set up filtering to only folders by specified owner (if given)
        if ($OwnerId !== null) {
            $this->OwnerId = intval($OwnerId);
            $this->setOrderOpsCondition("OwnerId = ".$this->OwnerId);
        }
    }

    /**
     * Create new folder that will contain only one type of item.
     * @param mixed $ItemType Type of item that folder will contain.
     * @param string $FolderName String containing name of folder.  (OPTIONAL)
     * @param int $OwnerId Numerical ID of folder owner.  (OPTIONAL)
     * @return Folder New folder object.
     */
    public function createFolder(
        $ItemType,
        ?string $FolderName = null,
        ?int $OwnerId = null
    ): Folder {
        # retrieve numerical item type
        $ItemTypeId = ($ItemType === Folder::MIXEDCONTENT)
                ? $ItemType : Folder::getItemTypeId($ItemType);

        # use default owner if available and none specified
        if (($OwnerId === null) & ($this->OwnerId !== null)) {
            $OwnerId = $this->OwnerId;
        }

        # add new folder to database
        $this->DB->query("INSERT INTO Folders SET"
                ." ContentType = ".$ItemTypeId
                .($FolderName ? ", FolderName = '".addslashes($FolderName)."'" : "")
                .(($OwnerId !== null) ? ", OwnerId = ".intval($OwnerId) : ""));

        # retrieve ID of new folder
        $Id = $this->DB->getLastInsertId();

        # create new folder object and return it to caller
        return new Folder($Id);
    }

    /**
     * Create new folder that can contain multiple types of items.  (This is a
     * separate operation because mixed item types incurs execution overhead.)
     * @param string $FolderName String containing name of folder.  (OPTIONAL)
     * @param int $OwnerId Numerical ID of folder owner.  (OPTIONAL)
     * @return Folder New folder object.
     */
    public function createMixedFolder(
        ?string $FolderName = null,
        ?int $OwnerId = null
    ): Folder {
        # create new mixed-content folder and return it to caller
        return $this->createFolder(Folder::MIXEDCONTENT, $FolderName, $OwnerId);
    }

    /**
     * Get total number of folders currently existing.
     * @return int Number of folders.
     */
    public function getFolderCount(): int
    {
        return $this->getItemCount(isset($this->OwnerId)
                ? "OwnerId = ".intval($this->OwnerId) : null);
    }

    /**
     * Retrieve folder with specified normalized name (as generated by
     * Folder::normalizeFolderName() method).
     * @param string $NormalizedName Normalized folder name.
     * @param int $OwnerId ID of folder owner.  (OPTIONAL)
     * @return Folder|null Folder object or NULL if no folder found.  If multiple
     *       folders with the specified name are found, the one with the lowest
     *       folder ID is returned.
     */
    public function getFolderByNormalizedName(string $NormalizedName, ?int $OwnerId = null)
    {
        # use default owner if available and none specified
        if (($OwnerId === null) & ($this->OwnerId !== null)) {
            $OwnerId = $this->OwnerId;
        }

        # query database for folder ID
        $FolderId = $this->DB->queryValue(
            "SELECT FolderId FROM Folders"
                    ." WHERE NormalizedName = '".addslashes($NormalizedName)."'"
                    .(($OwnerId !== null) ? " AND OwnerId = ".$this->OwnerId : "")
                    ." ORDER BY FolderId ASC",
            "FolderId"
        );

        # if folder found with specified name and owner
        if ($FolderId !== false && !is_null($FolderId)) {
            # create folder object and return it to caller
            return new Folder($FolderId);
        } else {
            # return NULL to caller to indicate folder not found
            return null;
        }
    }

    /**
     * Retrieve folders containing specified item.
     * @param object|int $Item Object (must have Id() method) or item ID.
     * @param mixed $ItemType The item type.
     * @param int $OwnerId Optional owner ID to restrict folders to.
     * @param bool $SharedFoldersOnly Whether to only return shared folders.
     * @return array Array of Folder objects that contain specified item.
     */
    public function getFoldersContainingItem(
        $Item,
        $ItemType,
        ?int $OwnerId = null,
        bool $SharedFoldersOnly = false
    ): array {

        # assume we won't find any folders
        $Folders = [];

        # retrieve item ID
        if (is_object($Item)) {
            if (method_exists($Item, "id")) {
                $ItemId = $Item->id();
            } else {
                throw new InvalidArgumentException(
                    "Item supplied of class without id() method."
                );
            }
        } elseif (is_numeric($Item)) {
            $ItemId = $Item;
        } else {
            throw new InvalidArgumentException("Item supplied of unsupported type.");
        }

        # retrieve numerical item type
        $ItemTypeId = ($ItemType === Folder::MIXEDCONTENT)
                ? $ItemType : Folder::getItemTypeId($ItemType);

        # use default owner if available and none specified
        if (($OwnerId === null) & ($this->OwnerId !== null)) {
            $OwnerId = $this->OwnerId;
        }

        # query database for IDs of all folders that contain item
        $this->DB->query("
            SELECT DISTINCT FolderItemInts.FolderId
            FROM FolderItemInts
            LEFT JOIN Folders
            ON FolderItemInts.FolderId = Folders.FolderId
            WHERE FolderItemInts.ItemId = '".intval($ItemId)."'
            AND (FolderItemInts.ItemTypeId = '".intval($ItemTypeId)."'
                 OR Folders.ContentType = '".intval($ItemTypeId)."')
            ".(($OwnerId !== null) ? " AND Folders.OwnerId = ".intval($OwnerId) : "")."
            ".(($SharedFoldersOnly) ? " AND Folders.IsShared = 1" : ""));
        $FolderIds = $this->DB->fetchColumn("FolderId");

        # create array of folders from folder IDs
        foreach ($FolderIds as $Id) {
            $Folders[$Id] = new Folder($Id);
        }

        # return folders (if any) to caller
        return $Folders;
    }

    /**
     * Retrieve folders with specified name, owner, or default content type.  If
     * no parameters are specified, all existing folders are returned.  If no owner
     * ID parameter is supplied and an owner ID was specified for
     * FolderFactory::folderFactory(), then that owner ID is used.
     * @param mixed $ItemType String containing type of item to search for as default
     *       content type of folder.  To search for only mixed-content-type
     *       folders specify Folder::MIXEDCONTENT.  (OPTIONAL, defaults to NULL)
     * @param int $OwnerId Numerical ID of folder owner.  (OPTIONAL, defaults to NULL)
     * @param string $Name String containing target folder name.  (OPTIONAL,
     *       defaults to NULL)
     * @param int $Offset Zero-based offset into list of folders.  (OPTIONAL)
     * @param int $Count Number of folders to retrieve beginning at specified
     *       offset.  (OPTIONAL)
     * @return array Array of Folder objects that match specified parameters.
     */
    public function getFolders(
        $ItemType = null,
        ?int $OwnerId = null,
        ?string $Name = null,
        int $Offset = 0,
        ?int $Count = null
    ): array {

        # retrieve numerical item type
        $ItemTypeId = ($ItemType === Folder::MIXEDCONTENT)
                ? $ItemType : Folder::getItemTypeId($ItemType);

        # retrieve IDs of all folders that match specified parameters
        $Condition = ($ItemTypeId !== null) ? "ContentType = ".intval($ItemTypeId) : null;
        if (($OwnerId !== null) || ($this->OwnerId !== null)) {
            $Condition .= ($Condition ? " AND " : "")."OwnerId = "
                    .intval(($OwnerId !== null) ? $OwnerId : $this->OwnerId);
        }
        if ($Name !== null) {
            $Condition .= ($Condition ? " AND " : "")."FolderName = '"
                    .addslashes($Name)."'";
        }
        $FolderIds = $this->getItemIds($Condition, false, "FolderId");

        # pare down list to requested range
        if ($Offset || $Count) {
            $FolderIds = $Count ? array_slice($FolderIds, $Offset, $Count)
                    : array_slice($FolderIds, $Offset);
        }

        # create array of folders based on IDs
        $Folders = [];
        foreach ($FolderIds as $FolderId) {
            $Folders[$FolderId] = new Folder($FolderId);
        }

        # return folders (if any) to caller
        return $Folders;
    }

    /**
     * Filter a list of folders to remove those with no publicly visible items
     *     in them.
     * @param array $FolderIds List of folders.
     * @param array $SchemaIds Schemas to check. (OPTIONAL, defaults to the
     *     Resource schema).
     * @return array $Olders that contain public items.
     */
    public static function filterOutFoldersWithNoPublicItems(
        array $FolderIds,
        array $SchemaIds = [MetadataSchema::SCHEMAID_DEFAULT]
    ) : array {
        $Result = [];

        foreach ($FolderIds as $FolderId) {
            $Folder = new Folder($FolderId);

            $ItemIdsBySchema = RecordFactory::buildMultiSchemaRecordList(
                $Folder->getVisibleItemIds(
                    User::getAnonymousUser()
                )
            );

            foreach ($SchemaIds as $SchemaId) {
                if (isset($ItemIdsBySchema[$SchemaId])) {
                    $Result[] = $FolderId;
                    continue 2;
                }
            }
        }

        return $Result;
    }


    # ---- PRIVATE INTERFACE -------------------------------------------------

    protected $OwnerId;
}
