<?php
    /**
     * Class: Page
     * The Page model.
     *
     * See Also:
     *     <Model>
     */
    class Page extends Model {
        const STATUS_LISTED  = "listed";
        const STATUS_PUBLIC  = "public";
        const STATUS_TEASED  = "teased";
        const STATUS_PRIVATE = "private";

        public $belongs_to = array(
            "user",
            "parent" => array("model" => "page")
        );

        public $has_many = array(
            "children" => array("model" => "page", "by" => "parent")
        );

        /**
         * Function: __construct
         *
         * See Also:
         *     <Model::grab>
         */
        public function __construct($page_id, $options = array()) {
            if (!isset($page_id) and empty($options))
                return;

            parent::grab($this, $page_id, $options);

            if ($this->no_results)
                return;

            $this->slug = $this->url;
            $this->filtered = (!isset($options["filter"]) or $options["filter"]);

            if ($this->public) {
                $this->status = ($this->show_in_list) ?
                    self::STATUS_LISTED :
                    self::STATUS_PUBLIC ;
            } else {
                $this->status = ($this->show_in_list) ?
                    self::STATUS_TEASED :
                    self::STATUS_PRIVATE ;
            }

            Trigger::current()->filter($this, "page");

            if ($this->filtered)
                $this->filter();
        }

        /**
         * Function: find
         *
         * See Also:
         *     <Model::search>
         */
        public static function find(
            $options = array(),
            $options_for_object = array()
        ): array {
            return parent::search(
                self::class,
                $options,
                $options_for_object
            );
        }

        /**
         * Function: add
         * Adds a page to the database.
         *
         * Parameters:
         *     $title - The Title for the new page.
         *     $body - The Body for the new page.
         *     $user - The <User> or <User.id> of the page's author.
         *     $parent_id - The ID of the new page's parent page (0 for none).
         *     $public - Whether the page can be viewed without permission.
         *     $show_in_list - Whether or not to show it in the pages list.
         *     $list_order - The order of the page in the list.
         *     $clean - The slug for this page.
         *     $url - The unique URL (created from $clean by default).
         *     $created_at - The new page's "created" timestamp.
         *     $updated_at - The new page's "last updated" timestamp.
         *
         * Returns:
         *     The newly created <Page>.
         *
         * Notes:
         *     The caller is responsible for validating all supplied values.
         *
         * See Also:
         *     <update>
         */
        public static function add(
            $title,
            $body,
            $user         = null,
            $parent_id    = 0,
            $public       = true,
            $show_in_list = true,
            $list_order   = 0,
            $clean        = "",
            $url          = "",
            $created_at   = null,
            $updated_at   = null
        ): self {
            $user_id = ($user instanceof User) ? $user->id : $user ;

            fallback($user_id,      Visitor::current()->id);
            fallback($parent_id,    0);
            fallback($public,       true);
            fallback($show_in_list, true);
            fallback($list_order,   0);
            fallback($clean,        slug(8));
            fallback($url,          self::check_url($clean));
            fallback($created_at,   datetime());
            fallback($updated_at,   SQL_DATETIME_ZERO); # Model->updated will check this.

            $sql = SQL::current();
            $trigger = Trigger::current();

            $new_values = array(
                "title"        => $title,
                "body"         => $body,
                "user_id"      => $user_id,
                "parent_id"    => $parent_id,
                "public"       => $public,
                "show_in_list" => $show_in_list,
                "list_order"   => $list_order,
                "clean"        => $clean,
                "url"          => $url,
                "created_at"   => $created_at,
                "updated_at"   => $updated_at
            );

            $trigger->filter($new_values, "before_add_page");
            $sql->insert(table:"pages", data:$new_values);
            $page = new self($sql->latest("pages"));
            $trigger->call("add_page", $page);
            return $page;
        }

        /**
         * Function: update
         * Updates the page.
         *
         * Parameters:
         *     $title - The new Title.
         *     $body - The new Body.
         *     $user - The <User> or <User.id> of the page's author.
         *     $parent_id - The new parent ID.
         *     $public - Whether the page can be viewed without permission.
         *     $show_in_list - Whether or not to show it in the pages list.
         *     $clean - A new slug for the page.
         *     $url - A new unique URL for the page (created from $clean by default).
         *     $created_at - The page's "created" timestamp.
         *     $updated_at - The page's "last updated" timestamp.
         *
         * Returns:
         *     The updated <Page>.
         *
         * Notes:
         *     The caller is responsible for validating all supplied values.
         */
        public function update(
            $title        = null,
            $body         = null,
            $user         = null,
            $parent_id    = null,
            $public       = null,
            $show_in_list = null,
            $list_order   = null,
            $clean        = null,
            $url          = null,
            $created_at   = null,
            $updated_at   = null
        ): self|false {
            if ($this->no_results)
                return false;

            $user_id = ($user instanceof User) ? $user->id : $user ;

            fallback(
                $title,
                ($this->filtered) ?
                                    $this->title_unfiltered :
                                    $this->title
            );
            fallback(
                $body,
                ($this->filtered) ?
                                    $this->body_unfiltered :
                                    $this->body
            );
            fallback($user_id,      $this->user_id);
            fallback($parent_id,    $this->parent_id);
            fallback($public,       $this->public);
            fallback($show_in_list, $this->show_in_list);
            fallback($list_order,   $this->list_order);
            fallback($clean,        $this->clean);
            fallback(
                $url,
                ($clean != $this->clean) ?
                                    self::check_url($clean) :
                                    $this->url
            );
            fallback($created_at,   $this->created_at);
            fallback($updated_at,   datetime());

            $sql = SQL::current();
            $trigger = Trigger::current();

            $new_values = array(
                "title"        => $title,
                "body"         => $body,
                "user_id"      => $user_id,
                "parent_id"    => $parent_id,
                "public"       => $public,
                "show_in_list" => $show_in_list,
                "list_order"   => $list_order,
                "clean"        => $clean,
                "url"          => $url,
                "created_at"   => $created_at,
                "updated_at"   => $updated_at
            );

            $trigger->filter($new_values, "before_update_page");

            $sql->update(
                table:"pages",
                conds:array("id" => $this->id),
                data:$new_values
            );

            $page = new self(
                null,
                array(
                    "read_from" => array_merge(
                        $new_values,
                        array("id" => $this->id)
                    )
                )
            );

            $trigger->call("update_page", $page, $this);
            return $page;
        }

        /**
         * Function: delete
         * Deletes the given page.
         *
         * Parameters:
         *     $page_id - The ID of the page to delete.
         *     $recursive - Should the page's children be deleted? (default: false)
         *
         * See Also:
         *     <Model::destroy>
         */
        public static function delete($page_id, $recursive = false): void {
            if ($recursive) {
                $page = new self($page_id);

                foreach ($page->children as $child)
                    self::delete($child->id);
            }

            parent::destroy(self::class, $page_id);
        }

        /**
         * Function: exists
         * Checks if a page exists.
         *
         * Parameters:
         *     $page_id - The page ID to check
         */
        public static function exists($page_id): bool {
            return SQL::current()->count(
                tables:"pages",
                conds:array("id" => $page_id)
            ) == 1;
        }

        /**
         * Function: check_url
         * Checks if a given URL value is already being used as another page's URL.
         *
         * Parameters:
         *     $url - The URL to check.
         *
         * Returns:
         *     The unique version of $url.
         *     If unused, it's the same as $url. If used, a number is appended to it.
         */
        public static function check_url($url): string {
            if (empty($url))
                return $url;

            $count = 1;
            $unique = substr($url, 0, 128);

            while (
                SQL::current()->count(
                    tables:"pages",
                    conds:array("url" => $unique)
                )
            ) {
                $count++;
                $unique = substr($url, 0, (127 - strlen($count)))."-".$count;
            }

            return $unique;
        }

        /**
         * Function: filter
         * Filters the page attributes through filter_page and markup filters.
         */
        private function filter(): void {
            $trigger = Trigger::current();
            $trigger->filter($this, "filter_page");

            $this->title_unfiltered = $this->title;
            $this->body_unfiltered = $this->body;

            $trigger->filter($this->title, array("markup_page_title", "markup_title"), $this);
            $trigger->filter($this->body, array("markup_page_text", "markup_text"), $this);
        }

        /**
         * Function: from_url
         * Attempts to grab a page from its clean or dirty  URL.
         *
         * Parameters:
         *     $request - The request URI to parse.
         *     $route - The route to respond to, or null to return a Page.
         */
        public static function from_url($request, $route = null): self|array|false {
            # Dirty URL?
            if (preg_match("/(\?|&)url=([^&#]+)/", $request, $slug)) {
                $page = new self(array("url" => $slug[2]));

                return isset($route) ?
                    $route->try["page"] = array($page) :
                    $page ;
            }

            $hierarchy = explode(
                "/",
                trim(str_replace(Config::current()->url, "/", $request), "/")
            );

            $pages = self::find(
                array("where" => array("url" => $hierarchy))
            );

            # One of the URLs in the page hierarchy is invalid.
            if (!(count($pages) == count($hierarchy)))
                return false;

            # Loop over the pages until we find the one we want.
            foreach ($pages as $page) {
                if ($page->url == end($hierarchy))
                    return isset($route) ?
                        $route->try["page"] = array($page) :
                        $page ;
            }
        }

        /**
         * Function: url
         * Returns a page's URL.
         */
        public function url(): string|false {
            if ($this->no_results)
                return false;

            $config = Config::current();

            if (!$config->clean_urls)
                return fix(
                    $config->url."/?action=page&url=".urlencode($this->url),
                    true
                );

            $url = array("", urlencode($this->url));

            $page = $this;

            while (isset($page->parent_id) and $page->parent_id > 0) {
                $url[] = urlencode($page->parent->url);
                $page = $page->parent;
            }

            return fix(
                $config->url."/".implode("/", array_reverse($url)),
                true
            );
        }

        /**
         * Function: author
         * Returns a page's author. Example: $page->author->name
         */
        public function author(): object|false {
            if ($this->no_results)
                return false;

            $author = (!$this->user->no_results) ?
                array(
                    "id"        => $this->user->id,
                    "name"      => oneof($this->user->full_name, $this->user->login),
                    "website"   => $this->user->website,
                    "email"     => $this->user->email,
                    "joined"    => $this->user->joined_at
                )
                :
                array(
                    "id"      => 0,
                    "name"    => __("[Guest]"),
                    "website" => "",
                    "email"   => "",
                    "joined"  => $this->created_at
                )
                ;

            return (object) $author;
        }
    }