<?php

/**
 * Defines an Upload Category.
 *
 * @package CGBetterUploads
 * @category Module
 * @author  calguy1000 <calguy1000@cmsmadesimple.org>
 * @copyright Copyright 2019 by Robert Campbell
 */

namespace CGBetterUploads;

/**
 * A class to represent a selection of files.
 * In this module a category is synonymous to a directory.  However no hierarchical relationship exists.  The directory name when created
 * in the filesystem is obfuscated.
 * There are a number of different types of categories, with different features.
 *
 * This is an immutable object, properties cannot be directly modified.
 *
 * @package CGBetterUploads
 * @property-read string $guid The unique, randomly generated id of this category
 * @property-read string $name A readable name for this category
 * @property-read string $description A brief, formatted description for this category.
 * @property-read string $type The type of category.  One of public, yousendit, limittofeu, or hidden.
 * @property-read bool $allow_upload Whether this category allows site visitors to upload files.
 * @property-read bool $allow_feu_delete Whether this category allows authorized FEU users to delete files
 * @property-read int  $expiry_hrs The number of hours before files in this category expiry and can be auto removed.  Default is 0.
 * @property-read string[] $allowed_feu_groups An array of FEU group names indicating which groups can upload files to this category.
 * @property-read string $allowed_mime_types A comma and newline separated list of mime types (wildcard permitted) that can be uploaded to this category.
 */
class Category
{
    /**
     * Indicates a public category.
     * Public categories can appear in the category list.  Files in these categories can be listed by site visitors.
     * Admins can optionally allow site visitors to upload files to these categories, and files can optionally automatically expire.
     */
    const TYPE_PUBLIC = 'public';         // files can be listed, optional feu upload, optional auto expiry (category setting)

    /**
     * Indicates a yousendit category.
     * These categories do not appear in the category list.  Files in these categories cannot be listed by site visotors.
     * Site visitors can upload files in these categories, and files automatically expire.
     */
    const TYPE_YOUSENDIT = 'yousendit';   // files cannot be listed, anonymous users can upload, auto expiry (config entry)

    /**
     * Indicates a category limited to FEU users.
     *
     * These categories will only appear in the category list if the user is logged in to FEU and authorized to see files in the category.
     * Access to these categories can be limited to members of certain FEU groups.  Upload to these categories can be enabled,
     * and optionally files can expire in these categories.
     */
    const TYPE_FEULIMITED = 'limittofeu'; // must at least be logged in to feu to view, optional feu upload, optional auto expiry (category setting)

    /**
     * Indicates a hidden category.
     *
     * Files in these categories can never be listed by site visitors.  Upload to these categories by site visitors can be enabled, and
     * files can auto expire.
     */
    const TYPE_HIDDEN = 'hidden';         // files cannot be listed, optional feu upload, optional auto expiry (category setting)

    /**
     * @ignore
     */
    private $_data = [ 'guid'=>null, 'name'=>null, 'description'=>null, 'type'=>self::TYPE_PUBLIC ];

    /**
     * @ignore
     */
    private $_extra;

    /**
     * @ignore
     */
    protected function __construct()
    {
        $this->_extra['allow_upload'] = true;
        $this->_extra['expiry_hrs'] = 0;
    }

    /**
     * @ignore
     */
    public function __get( string $key )
    {
        switch( $key ) {
        case 'guid':
        case 'name':
        case 'description':
        case 'type':
            return trim($this->_data[$key]);
        case 'allow_upload':
            // allow uploads to this category
            if( isset($this->_extra[$key])) return (bool) $this->_extra[$key];
            return;
        case 'allow_feu_delete':
            // allow authorized feu users to delete files in this category
            if( isset($this->_extra[$key])) return (bool) $this->_extra[$key];
            return;
        case 'expiry_hrs':
            // if not zero, auto-delete files that are older than this number of hours
            if( isset($this->_extra[$key])) return (int) $this->_extra[$key];
            return;
        case 'allowed_feu_groups':
            // if not empty, only members of these groups can view or upload files in this category
            if( isset($this->_extra[$key]) ) return (array) $this->_extra[$key];
            return;
        case 'allowed_mime_types':
            if( isset($this->_extra[$key]) ) return trim($this->_extra[$key]);
            return;
        default:
            throw new \LogicException("$key is not a gettable property of ".__CLASS__);
        }
    }

    /**
     * @internal
     */
    public function get_extra() : string
    {
        return json_encode($this->_extra);
    }

    /**
     * @ignore
     */
    public function __set( string $key, $val )
    {
        throw new \LogicException("$key is not a Settable property of ".__CLASS__);
    }

    /**
     * Adjust the name of the category object.
     *
     * @param string $name The new name
     * @return Category
     */
    public function with_name(string $name) : Category
    {
        $name = trim($name);
        if( !$name ) throw new \InvalidArgumentException('Invalid name passed to '.__METHOD__);
        $obj = clone $this;
        $obj->_data['name'] = $name;
        return $obj;
    }

    /**
     * Adjust the description of the category object.
     *
     * @param string $description The new description
     * @return Category
     */
    public function with_description(string $description) : Category
    {
        $obj = clone $this;
        $obj->_data['description'] = trim($description);
        return $obj;
    }

    /**
     * Adjust the type of the category object.
     *
     * @param string $type
     * @return Category
     */
    public function with_type(string $type) : Category
    {
        $obj = clone $this;
        switch( $type ) {
        case self::TYPE_PUBLIC:
        case self::TYPE_HIDDEN:
        case self::TYPE_YOUSENDIT:
        case self::TYPE_FEULIMITED:
            $obj->_data['type'] = $type;
            break;
        default:
            throw new \InvalidArgumentException("$type is not a valid category type");
        }
        return $obj;
    }

    /**
     * Whether or not to allow uploads to this category by site visitors.
     *
     * @param bool $flag
     * @return Category
     */
    public function with_allow_upload( bool $flag ) : Category
    {
        $obj = clone $this;
        $obj->_extra['allow_upload'] = $flag;
        return $obj;
    }

    /**
     * Whether or not authorized FEU users can remove files in this category.
     *
     * @param bool $flag
     * @return Category
     */
    public function with_allow_feu_delete( bool $flag ) : Category
    {
        $obj = clone $this;
        $obj->_extra['allow_feu_delete'] = $flag;
        return $obj;
    }

    /**
     * Set the number of hours before files in this category expire and can be auto-removed.
     *
     * @param int $hrs
     * @return Category
     */
    public function with_expiry_hrs( int $hrs ) : Category
    {
        $obj = clone $this;
        $obj->_extra['expiry_hrs'] = max(0,$hrs);
        return $obj;
    }

    /**
     * Set the list of FEU groups that are allowed to view this category.
     * This property is only used in feulimited categories.
     *
     * @param string[] $in
     * @return Category
     */
    public function with_allowed_feu_groups( array $in = null ) : Category
    {
        if( !empty($in) ) {
            foreach( $in as $one ) {
                if( !is_string($one) ) throw new \InvalidArgumentException('Invalid input to '.__METHOD__);
            }
            $in = array_values($in);
        }
        $obj = clone $this;
        $obj->_extra['allowed_feu_groups'] = $in;
        return $obj;
    }

    /**
     * Set the list of file types that are allowed to be uploaded to this category.
     *
     * @param string $data A comma separated list of mime types, wildcards are permitted.
     * @return Category
     */
    public function with_allowed_mime_types(string $data = null) : Category
    {
        $obj = clone $this;
        $obj->_extra['allowed_mime_types'] = $data;
        return $obj;
    }

    /**
     * @ignore
     */
    public function as_array() : array
    {
        $out = $this->_data;
        $out['extra'] = $this->get_extra();
        return $out;
    }

    /**
     * @ignore
     */
    public static function from_array( array $in ) : Category
    {
        $obj = new self;
        foreach( $in as $key => $val ) {
            switch( $key ) {
            case 'guid':
            case 'name':
            case 'description':
            case 'type':
                $obj->_data[$key] = trim($val);
                break;
            case 'extra':
                $tmp = json_decode($val, TRUE);
                if( is_array($tmp) && !empty($tmp) ) $obj->_extra = $tmp;
                break;
            }
        }
        return $obj;
    }

    /**
     * Whether uploading is permitted to files in this category.
     *
     * @return bool
     */
    public function can_upload() : bool
    {
        // tests whether this category allows uploads, does not determine user permissions
        switch( $this->type ) {
        case self::TYPE_PUBLIC:
        case self::TYPE_HIDDEN:
        case self::TYPE_FEULIMITED:
            return $this->allow_upload;
        case self::TYPE_YOUSENDIT:
            return true;
        }
        return false;
    }

    /**
     * Test whether an upload in this category can be considered to be epxired.
     * This method does not ensure a match between this categories guid, and the upload's category_guid
     *
     * @param Upload $upload
     * @return bool
     */
    public function is_upload_expired(Upload $upload) : bool
    {
        if( $this->expiry_hrs < 1 ) return false;
        return $upload->created < time() - $category->expiry_hrs * 3600;
    }
} // end of class
