<?php
namespace MatthiasWeb\RealMediaLibrary\attachment;

use MatthiasWeb\RealMediaLibrary\api\IStructure;
use MatthiasWeb\RealMediaLibrary\base\UtilsProvider;
use MatthiasWeb\RealMediaLibrary\folder\CRUD;
use MatthiasWeb\RealMediaLibrary\folder\Root;
use MatthiasWeb\RealMediaLibrary\view\View;

// @codeCoverageIgnoreStart
defined('ABSPATH') or die('No script kiddies please!'); // Avoid direct file request
// @codeCoverageIgnoreEnd

/**
 * This class handles all hooks and functions for the structure.
 * If something will print out, this is a facade-wrapper function
 * for the class View (stored in private $view).
 */
class Structure implements IStructure {
    use UtilsProvider;

    /**
     * The structure should be accessible within one
     * or more blogs. For those purposes use the wp standard
     * method switch_to_blog();
     *
     * This is an array of Structure objects.
     */
    private static $blogs = [];

    /**
     * The root folder ID. Can only be set by the constructor!
     */
    private $root;

    /**
     * Array of database read rows
     */
    private $rows;

    private $parsed;

    private $tree;

    /**
     * The view handler for this structure
     */
    private $view;

    /**
     * Checks, if the folder tree is already loaded and do the initial
     * load if needed by an function. So, RML can guarantee lazy loading.
     */
    private $hasInitialLoad = false;

    /**
     * Additional data passed to the structure.
     */
    private $data;

    // Documented in IStructure
    public function __construct($root = null, $data = null) {
        $this->view = new View($this);
        $this->data = $data === null ? [] : $data;
        $this->resetData($root, false);
    }

    // Documented in IStructure
    public function initialLoad() {
        if (!$this->hasInitialLoad) {
            $this->hasInitialLoad = true;
            $this->resetData($this->root);
        }
    }

    // Documented in IStructure
    public function resetData($root = null, $fetchData = true) {
        $this->root = $root === null ? _wp_rml_root() : $root;
        $this->rows = [];
        $this->parsed = [];
        $this->tree = [];

        if ($fetchData) {
            $this->fetch();
        } else {
            $this->hasInitialLoad = false;
        }
    }

    /**
     * Fetching all available folders into an array.
     */
    private function fetch() {
        global $wpdb;

        $table_name = $this->getTableName();

        /**
         * Modify the tree SQL select fields statement. Just push your
         * fields to select custom fields.
         *
         * @param {array} $fields The standard RML fields
         * @param {IStructure} $structure The structure
         * @hook RML/Tree/SQLStatement/SELECT
         * @return {array}
         */
        $fields = join(
            ', ',
            apply_filters(
                'RML/Tree/SQLStatement/SELECT',
                [
                    // The whole row of the folder
                    'tn.*',
                    // Count images for this folder
                    'IFNULL(tn.cnt, (' . CountCache::getInstance()->getSingleCountSql() . ')) AS cnt_result',
                    'rml_meta_orderby.meta_value AS lastOrderBy',
                    'rml_meta_orderAutomatically.meta_value AS orderAutomatically',
                    'rml_meta_lastSubOrderBy.meta_value AS lastSubOrderBy',
                    'rml_meta_subOrderAutomatically.meta_value AS subOrderAutomatically',
                ],
                $this
            )
        );

        /**
         * Modify the tree SQL select join statement. Just push your
         * joins to $fields array.
         *
         * @param {array} $fields The standard RML fields
         * @param {IStructure} $structure The structure
         * @hook RML/Tree/SQLStatement/JOIN
         * @return {array} $fields
         */
        $joins = join(
            ' ',
            apply_filters(
                'RML/Tree/SQLStatement/JOIN',
                [
                    // The last order by, saved in folder meta as "orderby"
                    'LEFT JOIN ' .
                    $this->getTableName('meta') .
                    " rml_meta_orderby ON rml_meta_orderby.realmedialibrary_id = tn.ID AND rml_meta_orderby.meta_key = 'orderby'",
                    // Determines, if the orderby should be applied automatically, saved in folder meta as "orderAutomatically"
                    'LEFT JOIN ' .
                    $this->getTableName('meta') .
                    " rml_meta_orderAutomatically ON rml_meta_orderAutomatically.realmedialibrary_id = tn.ID AND rml_meta_orderAutomatically.meta_key = 'orderAutomatically'",
                    // The last subfolder order by, saved in folder meta as "lastSubOrderBy"
                    'LEFT JOIN ' .
                    $this->getTableName('meta') .
                    " rml_meta_lastSubOrderBy ON rml_meta_lastSubOrderBy.realmedialibrary_id = tn.ID AND rml_meta_lastSubOrderBy.meta_key = 'lastSubOrderBy'",
                    // Determines, if the sbufolder orderby should be applied automatically, saved in folder meta as "subOrderAutomatically"
                    'LEFT JOIN ' .
                    $this->getTableName('meta') .
                    " rml_meta_subOrderAutomatically ON rml_meta_subOrderAutomatically.realmedialibrary_id = tn.ID AND rml_meta_subOrderAutomatically.meta_key = 'subOrderAutomatically'",
                ],
                $this
            )
        );

        /**
         * Add WHERE statement to modify parts of the tree SQL statement.
         *
         * @param {string} $sql The sql query
         * @param {IStructure} $structure The structure
         * @hook RML/Tree/SQLStatement/WHERE
         * @return {string}
         * @since 4.0.8
         */
        $where = apply_filters('RML/Tree/SQLStatement/WHERE', '', $this);

        /**
         * Modify the full tree SQL statement.
         *
         * @param {string} $sql The sql query
         * @param {IStructure} $structure The structure
         * @hook RML/Tree/SQLStatement
         * @return {string}
         */
        $sqlStatement = apply_filters(
            'RML/Tree/SQLStatement',
            ["SELECT $fields FROM $table_name AS tn $joins $where ORDER BY parent, ord"],
            $this
        );

        // phpcs:disable WordPress.DB.PreparedSQL
        $this->rows = $wpdb->get_results($sqlStatement[0]);
        // phpcs:enable WordPress.DB.PreparedSQL

        /**
         * The tree content is loaded.
         *
         * @param {object[]} $rows The SQL results
         * @param {IStructure} $structure The structure
         * @hook RML/Tree/SQLRows
         * @return {object[]}
         */
        $this->rows = apply_filters('RML/Tree/SQLRows', $this->rows, $this);

        $this->parse();
    }

    /**
     * This functions parses the read rows into folder objects.
     * It also handles the `cnt` cache for the attachments in this folder.
     */
    private function parse() {
        /**
         * Use this hook to register your own creatables with the help of
         * wp_rml_register_creatable().
         *
         * @hook RML/Creatable/Register
         */
        do_action('RML/Creatable/Register');
        if (!empty($this->rows)) {
            $noCntCache = false;
            foreach ($this->rows as $key => $value) {
                // Check for image cache
                if (is_null($value->cnt)) {
                    $noCntCache = true;
                }

                // Prepare the data types
                $this->rows[$key]->id = intval($value->id);
                $this->rows[$key]->parent = intval($value->parent);
                $this->rows[$key]->ord = intval($value->ord);
                $this->rows[$key]->cnt_result = intval($value->cnt_result);

                // Craetable instance
                $creatable = CRUD::getInstance()->getCreatables($value->type);
                if (isset($creatable)) {
                    $result = call_user_func_array([$creatable, 'instance'], [$value]);
                    if (is_rml_folder($result)) {
                        $this->parsed[] = $result;
                    }
                }
            }

            if ($noCntCache) {
                CountCache::getInstance()->updateCountCache();
            }
        }

        /**
         * Here you can modify the fresh-created instances of an IFolder object. This allows you,
         * e. g. hide folders on frontend with `setVisible`.
         *
         * @param {IFolder[]} $parsed
         * @hook RML/Tree/Parsed
         * @return IFolder[]
         * @since 4.8.1
         */
        $this->parsed = apply_filters('RML/Tree/Parsed', $this->parsed);

        // Create the tree
        $folder = null;
        foreach ($this->parsed as $key => $category) {
            $parent = $category->getParent();
            if ($parent > -1) {
                $folder = $this->byId($parent);
                if ($folder !== null) {
                    $folder->addChildren($category);
                }
            }
        }

        $cats_tree = [];
        foreach ($this->parsed as $category) {
            if ($category->getParent() <= -1) {
                $cats_tree[] = $category;
            }
        }
        $this->tree = $cats_tree;
    }

    // Documented in IStructure
    public function byId($id, $nullForRoot = true) {
        $id = intval($id);
        if (!$nullForRoot && $id === -1) {
            return Root::getInstance();
        }

        foreach ($this->getParsed() as $folder) {
            if ($folder->getId() === $id) {
                return $folder;
            }
        }

        /**
         * When a folder is not found by an absolute path this filter is
         * called and looks up for folders which are perhaps handled by other
         * structures. If you are hooking into this function please consider that
         * you apply_filters for RML/Tree/Parsed manually.
         *
         * @param {IFolder} $folder The folder (null if not found)
         * @param {integer} $id The searched Id
         * @param {IStructure} $structure The structure
         * @hook RML/Tree/ResolveById
         * @return {IFolder} The found folder or null if not found
         * @since 3.3.1
         */
        return apply_filters('RML/Tree/ResolveById', null, $id, $this);
    }

    // Documented in IStructure
    public function byAbsolutePath($path) {
        $path = trim($path, '/');
        foreach ($this->getParsed() as $folder) {
            if (strtolower($folder->getAbsolutePath()) === strtolower($path)) {
                return $folder;
            }
        }

        /**
         * When a folder is not found by an absolute path this filter is
         * called and looks up for folders which are perhaps handled by other
         * structures. If you are hooking into this function please consider that
         * you apply_filters for RML/Tree/Parsed manually.
         *
         * @param {IFolder} $folder The folder (null if not found)
         * @param {string} $path The searched path
         * @param {IStructure} $structure The structure
         * @hook RML/Tree/ResolveByAbsolutePath
         * @return {IFolder} The found folder or null if not found
         * @since 3.3.1
         */
        return apply_filters('RML/Tree/ResolveByAbsolutePath', null, $path, $this);
    }

    // Documented in IStructure
    public function getRows() {
        $this->initialLoad();
        return $this->rows;
    }

    // Documented in IStructure
    public function getParsed() {
        $this->initialLoad();
        return $this->parsed;
    }

    // Documented in IStructure
    public function getTree() {
        $this->initialLoad();
        return $this->tree;
    }

    // Documented in IStructure
    public function getPlainTree() {
        $result = [];
        $tree = $this->getTree();
        if (is_array($tree)) {
            foreach ($tree as $obj) {
                $plain = $obj->getPlain(true);
                if ($plain !== null) {
                    $result[] = $plain;
                }
            }
        }
        return $result;
    }

    // Documented in IStructure
    public function getCntAttachments() {
        if (has_filter('RML/Tree/CountAttachments')) {
            /**
             * Counts all attachments which are available in the structure.
             *
             * @param {integer} $count The count
             * @param {object} $structure The structure class
             * @return {integer} The count
             * @hook RML/Tree/CountAttachments
             */
            return apply_filters('RML/Tree/CountAttachments', 0, $this);
        }
        return (int) wp_count_posts('attachment')->inherit;
    }

    /**
     * Get all folder counts.
     *
     * @return Array<string|int,int>
     */
    public function getFolderCounts() {
        $result = [];

        // Default folder counts
        $root = _wp_rml_root();
        $result['all'] = $this->getCntAttachments();
        $result[$root] = $this->getCntRoot();

        // Iterate through our folders
        $folders = $this->getParsed();
        if (is_array($folders)) {
            foreach ($folders as $value) {
                $id = $value->getId();
                $result[$id] = $value->getCnt();
            }
        }
        return $result;
    }

    // Documented in IStructure
    public function getCntRoot() {
        $cnt = 0;
        foreach ($this->getParsed() as $folder) {
            $cnt += $folder->getCnt();
        }
        $result = $this->getCntAttachments() - $cnt;
        return $result >= 0 ? $result : 0;
    }

    /**
     * Get the view class instance.
     */
    public function getView() {
        return $this->view;
    }

    // Documented in IStructure
    public function getData() {
        return $this->data;
    }

    // Documented in IStructure
    public function setData($data) {
        $this->data = is_array($data) ? $data : [];
    }

    /**
     * Get instance.
     *
     * @return Structure
     */
    public static function getInstance() {
        $bid = get_current_blog_id();
        return !isset(self::$blogs[$bid]) ? (self::$blogs[$bid] = new Structure()) : self::$blogs[$bid];
    }
}
