<?php

declare(strict_types=1);

namespace Bnomei;

use Kirby\Cms\Page;
use Kirby\Filesystem\Dir;
use Kirby\Toolkit\A;

final class Bolt
{
    /**
     * @var string
     */
    private $root;

    /**
     * @var string
     */
    private $extension;

    /**
     * @var array<string>
     */
    private $modelFiles;

    /**
     * @var Page|null
     */
    private $parent;

    /**
     * @var array<string,Page>
     */
    private static $idToPage;

    public function __construct(?Page $parent = null)
    {
        $kirby = kirby();
        $this->root = $kirby->root('content');
        if ($parent) {
            $this->parent = $parent;
            $this->root = $parent->root();
        }

        $this->extension = $kirby->contentExtension();
        if ($kirby->multilang()) {
            $this->extension = $kirby->defaultLanguage()->code().'.'.$this->extension;
        }

        $extension = $this->extension;
        $this->modelFiles = array_map(static function ($value) use ($extension) {
            return $value.'.'.$extension;
        }, array_keys(Page::$models));

        if (option('debug')) {
            $this->flush();
        }
    }

    private static function cache()
    {
        return BoostCache::singleton();
    }

    public function flush()
    {
        self::$idToPage = [];

        // this would flush content cache as well or depending on cache driver everything
        // do NOT do this ever!
        // // BoostCache::singleton()->flush();

        return true;
    }

    public function lookup(string $id, bool $cache = true): ?Page
    {
        $lookup = A::get(self::$idToPage, $id);

        // in case the page was deleted/moved
        if ($lookup && Dir::exists($lookup->root()) === false) {
            unset(self::$idToPage[$id]);
            $lookup = null;
        }

        if (! $lookup && $cache && self::cache()) {
            if ($diruri = self::cache()->get('bolt/'.hash(BoostCache::hashalgo(), $id))) {
                // bolt will ignore caches with invalid paths and update them automatically
                // it does not need to be flushed ever
                if ($page = $this->findByID($diruri, false)) {
                    $this->pushLookup($id, $page);
                    $lookup = $page;
                } else {
                    self::cache()->remove('bolt/'.hash(BoostCache::hashalgo(), $id));
                }
            }
        }

        return $lookup;
    }

    public function pushLookup(string $id, Page $page): void
    {
        self::$idToPage[$id] = $page;

        // only update if necessary
        $diruri = $page->diruri();
        if ($diruri !== self::cache()->get('bolt/'.hash(BoostCache::hashalgo(), $id))) {
            self::cache()->set(hash(BoostCache::hashalgo(), $id).'-bolt', $diruri, option('bnomei.boost.expire'));
        }
    }

    public static function toArray(): array
    {
        if (! self::$idToPage) {
            self::$idToPage = [];
        }

        return self::$idToPage;
    }

    public function findByID(string $id, bool $cache = true, bool $extend = true): ?Page
    {

        $page = $this->lookup($id, $cache);
        if ($page) {
            return $page;
        }

        $draft = false;
        $treeid = null;
        $parent = $this->parent;
        $parts = explode('/', $id);

        foreach ($parts as $part) {
            if ($part === '_drafts') {
                $draft = true;
                $this->root .= '/'.'_drafts';

                continue;
            }
            $numSplit = array_reverse(explode(Dir::$numSeparator, $part));
            $partWithoutNum = $numSplit[0];
            $num = count($numSplit) > 1 ? intval($numSplit[1]) : null;
            $treeid = $treeid ? $treeid.'/'.$partWithoutNum : $partWithoutNum;
            $page = $this->lookup($treeid, $cache);
            if ($page && Dir::exists($page->root())) {
                $parent = $page;
                $this->root = $page->root(); // loop

                continue;
            }

            $params = [
                'root' => null,
                'dirname' => null,
                'parent' => $parent,
                'slug' => $partWithoutNum,
                'num' => $num,
                'model' => null,
            ];

            // if dir exists
            if (is_dir($this->root.'/'.$part)) {
                $params['root'] = $this->root.'/'.$part;
                $params['dirname'] = $part;
                foreach ($this->modelFiles as $modelFile) {
                    if (file_exists($params['root'].'/'.$modelFile)) {
                        $template = str_replace('.'.$this->extension, '', $modelFile);
                        $params['template'] = $template;
                        $params['model'] = $template;
                        break;
                    }
                }
            } else { // search for dir
                $directory = @opendir($this->root);
                if ($directory) {
                    while ($file = readdir($directory)) {
                        if (strpos($file, '.') !== false) {
                            continue;
                        }
                        $_part = Dir::$numSeparator.$part;
                        if (substr($file, -strlen($_part)) === $_part) {
                            $params['root'] = $this->root.'/'.$file;
                            $params['diruri'] = $file;
                            if (preg_match('/^([0-9]+)'.Dir::$numSeparator.'(.*)$/', $file, $match)) {
                                $params['num'] = intval($match[1]);
                                $params['slug'] = $match[2];
                            }
                        }
                        if ($params['root']) {
                            foreach ($this->modelFiles as $modelFile) {
                                if (file_exists($params['root'].'/'.$modelFile)) {
                                    $template = str_replace('.'.$this->extension, '', $modelFile);
                                    $params['template'] = $template;
                                    $params['model'] = $template;
                                    break;
                                }
                            }
                            break;
                        }
                    }
                    closedir($directory);
                }
            }

            if (! $params['root']) {
                return null; // not found
            }
            if ($draft === true) {
                $params['isDraft'] = $draft;
                // Only direct subpages are marked as drafts
                $draft = false;
            }
            $page = null; // kirby()->extension('pages', $this->root);
            if (! $page) {
                $page = Page::factory($params);
                $this->pushLookup($treeid, $page);

                if ($extend) {
                    kirby()->extend([
                        'pages' => [$this->root => $page],
                    ]);
                }
            }
            $parent = $page;
            $this->root = $params['root']; // loop
        }

        return $page;
    }

    public static function page(string $id, ?Page $parent = null, bool $cache = true, bool $extend = true): ?Page
    {
        return (new self($parent))->findByID($id, $cache, $extend);
    }

    public static function index($callback)
    {
        $count = 0;
        foreach (site()->siteindexfolders() as $page) {
            // save memory when indexing
            $page = \Bnomei\Bolt::page($page, null, false, false);
            if ($page && ! is_string($callback) && is_callable($callback)) {
                $callback($page);
            }
            $count++;
            $page = null; // free memory, do not use unset()
        }

        return $count;
    }
}
