<?php

namespace ofc;

use PostTypes\PostType;
use Handlebars\Handlebars;
use Handlebars\Loader\FilesystemLoader;
use PostTypes\Taxonomy;
use WP_Post;

class Site
{
    private $config;
    private $hb;
    private $partialsDir;
    private $fileExtension;
    private $templateDirectory;
    public $cptSlugs = [];

    public function __construct($config = [])
    {
        if ($config == [] && file_exists(TEMPLATEPATH . "/config.php")) {
            $config = include(TEMPLATEPATH . "/config.php");
        }

        $this->config = $config;
        $this->bootstrap();
    }

    private function bootstrap()
    {
        // wordpress added this style for us, but we don't want it
        add_action('wp_enqueue_scripts', function () {
            wp_dequeue_style('classic-theme-styles');
        }, 20);

        if (isset($this->config["excerpt_more_text"])) {
            add_filter('excerpt_more', fn ($more) => $this->config["excerpt_more_text"]);
        }

        // disable various things
        if (isset($this->config["disable"])) {
            $this->disable($this->config["disable"]);
        }

        // enable various things, starting with TinyMCE
        if (isset($this->config["tinyMCEAdditions"]) || isset($this->config["tiny-mce-additions"])) {
            $this->addEditorStyles();
        }
        if (isset($this->config["enable"])) {
            $this->enable($this->config["enable"]);
        }

        // adjust wordpress filters
        $this->processFilters();

        // create wordpress menu locations
        $this->registerMenuLocations();

        // initialize handlebars
        $this->setUpHandlebars();

        // register custom post types
        $this->registerCPTs();

        // add custom shortcodes
        $this->registerShortcodes();

        // add mix static assets
        $this->includeManifestFiles();

        // register ACF options pages
        $this->addOptionsPages();

        // register custom fields that aren't through ACF
        $this->processCustomFields();

        // register any ajax callbacks
        $this->processAdminAJAX();
        $this->processGuestAJAX();
    }

    public function getFlexFilePrefix(): string
    {
        if (!isset($this->config["flex-file-prefix"])) {
            return "";
        }
        return $this->config["flex-file-prefix"]."_";
    }

    private function processGuestAJAX()
    {
        if (!isset($this->config["guest-ajax"])) {
            return;
        }
        if (!is_array($this->config["guest-ajax"])) {
            return;
        }
        foreach ($this->config["guest-ajax"] as $hook => $callback) {
            // guest ajax methos work both for guests and authed users
            add_action("wp_ajax_nopriv_$hook", function () use ($callback) {
                echo json_encode($callback());
                wp_die();
            });
            add_action("wp_ajax_$hook", function () use ($callback) {
                echo json_encode($callback());
                wp_die();
            });
        }
    }

    private function processAdminAJAX()
    {
        if (!isset($this->config["admin-ajax"])) {
            return;
        }
        if (!is_array($this->config["admin-ajax"])) {
            return;
        }
        foreach ($this->config["admin-ajax"] as $hook => $callback) {
            add_action("wp_ajax_$hook", function () use ($callback) {
                echo json_encode($callback());
                wp_die();
            });
        }
    }

    private function addEditorStyles()
    {
        $formats = [];

        if (isset($this->config["tinyMCEAdditions"])) {
            $formats = $this->config["tinyMCEAdditions"];
        }

        if (isset($this->config["tiny-mce-additions"])) {
            $formats = $this->config["tiny-mce-additions"];
        }

        $defaults = [
            [
                'title' => 'Headings',
                'items' => [
                    [
                        'title' => 'Heading 1',
                        'format' => 'h1',
                    ],
                    [
                        'title' => 'Heading 2',
                        'format' => 'h2',
                    ],
                    [
                        'title' => 'Heading 3',
                        'format' => 'h3',
                    ],
                    [
                        'title' => 'Heading 4',
                        'format' => 'h4',
                    ],
                    [
                        'title' => 'Heading 5',
                        'format' => 'h5',
                    ],
                    [
                        'title' => 'Heading 6',
                        'format' => 'h6',
                    ],
                ],
            ],
            [
                'title' => 'Inline',
                'items' => [
                    [
                        'title' => 'Bold',
                        'format' => 'bold',
                        'icon' => 'bold',
                    ],
                    [
                        'title' => 'Italic',
                        'format' => 'italic',
                        'icon' => 'italic',
                    ],
                    [
                        'title' => 'Underline',
                        'format' => 'underline',
                        'icon' => 'underline',
                    ],
                    [
                        'title' => 'Strikethrough',
                        'format' => 'strikethrough',
                        'icon' => 'strikethrough',
                    ],
                    [
                        'title' => 'Superscript',
                        'format' => 'superscript',
                        'icon' => 'superscript',
                    ],
                    [
                        'title' => 'Subscript',
                        'format' => 'subscript',
                        'icon' => 'subscript',
                    ],
                    [
                        'title' => 'Code',
                        'format' => 'code',
                        'icon' => 'code',
                    ],
                ],
            ],
            [
                'title' => 'Blocks',
                'items' => [
                    [
                        'title' => 'Paragraph',
                        'format' => 'p',
                    ],
                    [
                        'title' => 'Blockquote',
                        'format' => 'blockquote',
                    ],
                    [
                        'title' => 'Div',
                        'format' => 'div',
                    ],
                    [
                        'title' => 'Pre',
                        'format' => 'pre',
                    ],
                ],
            ],
            [
                'title' => 'Alignment',
                'items' => [
                    [
                        'title' => 'Left',
                        'format' => 'alignleft',
                        'icon' => 'alignleft',
                    ],
                    [
                        'title' => 'Center',
                        'format' => 'aligncenter',
                        'icon' => 'aligncenter',
                    ],
                    [
                        'title' => 'Right',
                        'format' => 'alignright',
                        'icon' => 'alignright',
                    ],
                    [
                        'title' => 'Justify',
                        'format' => 'alignjustify',
                        'icon' => 'alignjustify',
                    ],
                ],
            ],
        ];

        add_filter('tiny_mce_before_init', function ($settings) use ($formats, $defaults) {
            $settings['style_formats'] = json_encode(array_merge($defaults, [
                [
                    "title" => "Custom",
                    "items" => $formats
                ]
            ]));
            return $settings;
        });
    }

    private function includeManifestFiles()
    {
        $manifestFile = get_template_directory() . "/dist/mix-manifest.json";
        if (!file_exists($manifestFile)) {
            return;
        }

        $items = json_decode(file_get_contents($manifestFile), true);
        foreach ($items as $file => $version) {
            $extension = end(explode(".", $file));
            $tag = $this->stringify($file);
            if ($extension === "js") {
                add_action('wp_enqueue_scripts', function () use ($version, $tag) {
                    wp_register_script($tag, get_template_directory_uri() . '/dist' . $version, [], '', true);
                    wp_enqueue_script($tag);
                });
                continue;
            }

            if ($extension === "css" && $file != "/inline.css") {
                add_action('wp_enqueue_scripts', function () use ($version, $tag) {
                    wp_enqueue_style($tag, get_template_directory_uri() . '/dist' . $version);
                });
                continue;
            }

            if ($file == "/inline.css") {
                add_action('wp_head', function () use ($file) {
                    $css = file_get_contents(get_template_directory() . "/dist" . $file);
                    echo "<style>".$css."</style>";
                });
            }
        }
    }

    private function registerShortcodes()
    {
        if (!array_key_exists('shortcodes', $this->config)) {
            return;
        }

        if (!is_array($this->config["shortcodes"])) {
            return;
        }

        foreach ($this->config["shortcodes"] as $name => $callable) {
            add_shortcode($name, $callable);
        }
    }

    private function registerCPTs()
    {
        if (!array_key_exists('custom-post-types', $this->config)) {
            return;
        }

        $cptTaxonomies = [];
        foreach ($this->config['custom-post-types'] as $cpt) {
            if (!isset($cpt['slug'])) {
                $this->adminError("To register a new custom post type, you must set the `slug` key.");
                continue;
            }

            // for convenience access
            $this->cptSlugs[] = $cpt["slug"];

            $options = $cpt['options'] ?? [];
            if (isset($cpt["archive"]) && $cpt["archive"]) {
                $options["has_archive"] = Util::slugify($this->humanize($cpt["slug"], true));
            }
            $names = $this->generateLabels($cpt['slug']);

            $newCpt = new PostType($cpt['slug'], $options, $names);

            if (isset($cpt['icon'])) {
                $newCpt->icon($cpt['icon']);
            }

            if (isset($cpt["taxonomies"])) {
                foreach ($cpt["taxonomies"] as $customTaxonomy) {
                    $cptTaxonomies[] = $customTaxonomy;
                    $newCpt->taxonomy($customTaxonomy);
                }
            }
            $newCpt->register();

            // disable post type things here
            if (isset($cpt["disable"]) && is_array($cpt["disable"])) {
                foreach ($cpt["disable"] as $feature) {
                    if (strtolower($feature) === "yoast") {
                        add_action('add_meta_boxes', fn () => remove_meta_box('wpseo_meta', $cpt['slug'], 'normal'), 100);
                        continue;
                    }
                }
            }

            // set up any ACF CPT options pages here
            if (isset($cpt["options-pages"])) {
                if (!function_exists("acf_add_options_page")) {
                    $this->adminError("You can't register options pages without the Pro version of Advanced Custom Fields.");
                    continue;
                }
                foreach ($cpt["options-pages"] as $optionsPage) {
                    if (is_string($optionsPage)) {
                        acf_add_options_page([
                            "page_title" => $optionsPage,
                            "menu_title" => $optionsPage,
                            "menu_slug" => $this->stringify($optionsPage),
                            'capability' => 'edit_posts',
                            "position" => 100,
                            "parent_slug" => "edit.php?post_type=" . $cpt["slug"]
                        ]);
                        continue;
                    }

                    if (is_array($optionsPage) && isset($optionsPage["name"]) && isset($optionsPage["parent_slug"])) {
                        acf_add_options_page([
                            "page_title" => $optionsPage["name"],
                            "menu_title" => $optionsPage["name"],
                            "menu_slug" => $this->stringify($optionsPage["name"]),
                            'capability' => 'edit_posts',
                            "position" => 100,
                            "parent_slug" => $optionsPage["parent_slug"],
                        ]);
                    }
                }
            }
        }

        if (count($cptTaxonomies)) {
            foreach ($cptTaxonomies as $customTaxonomy) {
                $tax = new Taxonomy($customTaxonomy);
                $tax->options([
                    'hierarchical' => false,
                ]);
                $tax->register();
            }
        }
    }

    private function disable(array $keys)
    {
        foreach ($keys as $key) {
            if ($key === "editor") {
                define('DISALLOW_FILE_EDIT', true);
                continue;
            }
            if ($key === "gutenberg") {
                add_filter('use_block_editor_for_post', '__return_false', 10);
                add_action('wp_enqueue_scripts', function () {
                    wp_dequeue_style('wp-block-library');
                    wp_dequeue_style('wp-block-library-theme');
                    wp_dequeue_style('wc-blocks-style'); // Remove WooCommerce block CSS
                    wp_dequeue_style('global-styles');
                }, 100);
                continue;
            }
            if ($key === "patterns") {
                add_action('after_setup_theme', function () {
                    remove_theme_support('core-block-patterns');
                });
                add_action('admin_init', function () {
                    remove_submenu_page('themes.php', 'edit.php?post_type=wp_block');
                    remove_submenu_page('themes.php', 'site-editor.php?p=/pattern');
                    remove_submenu_page('themes.php', 'site-editor.php?p=/patterns');
                });
                continue;
            }
            if ($key === "meta-generator") {
                remove_action('wp_head', 'wp_generator');
                add_filter('the_generator', '__return_null');
                continue;
            }
            if ($key === "emojis") {
                remove_action('wp_head', 'print_emoji_detection_script', 7);
                remove_action('wp_print_styles', 'print_emoji_styles');
                continue;
            }
            if ($key === "customizer") {
                add_action('init', function () {
                    add_filter('map_meta_cap', function ($capabilities = [], $c = '', $user_id = 0, $args = []) {
                        if ($c == 'customize') {
                            return ['nope'];
                        }
                        return $capabilities;
                    }, 10, 4);
                }, 10);

                add_action('admin_init', function () {
                    remove_action('plugins_loaded', '_wp_customize_include', 10);
                    remove_action('admin_enqueue_scripts', '_wp_customize_loader_settings', 11);

                    add_action('load-customize.php', function () {
                        wp_die("The customizer is disabled via your current theme configuration.");
                    });
                }, 10);
                continue;
            }
        }
    }

    private function addOptionsPages()
    {
        if (!array_key_exists('options-pages', $this->config)) {
            return;
        }

        if (!function_exists("acf_add_options_page")) {
            $this->adminError("You can't register options pages without the Pro version of Advanced Custom Fields.");
        }

        $i = 100;
        foreach ($this->config["options-pages"] as $pageTitle) {
            acf_add_options_page([
                "page_title" => $pageTitle,
                "menu_title" => $pageTitle,
                "menu_slug" => $this->stringify($pageTitle),
                'capability' => 'edit_posts',
                "position" => $i,
            ]);
            $i++;
        }
    }

    private function enable(array $keys)
    {
        foreach ($keys as $key) {
            if ($key === "post-thumbnails") {
                add_theme_support('post-thumbnails');
                continue;
            }
            if ($key === "menus") {
                add_theme_support('menus');
                continue;
            }
            if ($key === "styleselect") {
                add_filter('mce_buttons_2', function ($buttons) {
                    return array_merge(['styleselect'], $buttons);
                });
                continue;
            }
            if ($key === "svg") {
                add_filter('upload_mimes', function ($file_types) {
                    $file_types['svg'] = 'image/svg+xml';
                    return $file_types;
                });
                continue;
            }

            $this->adminError("Couldn't enable feature `$key`");
        }
    }

    private function processFilters()
    {
        // handle "guest" html body class
        $guestClass = "guest";
        if (array_key_exists("guest-class", $this->config)) {
            $guestClass = $this->config["guest-class"];
        }
        if (!is_null($guestClass)) {
            add_filter('body_class', function ($classes) use ($guestClass) {
                return in_array('logged-in', $classes) ? $classes : [$guestClass, ...$classes];
            });
        }
    }

    private function registerMenuLocations()
    {
        if (!array_key_exists("menu-locations", $this->config)) {
            return;
        }

        if (!is_array($this->config["menu-locations"])) {
            // TODO: show a helpful error message that notifies the developer this is set up wrong
            return;
        }

        $menus = $this->config["menu-locations"];
        add_action('after_setup_theme', function () use ($menus) {
            register_nav_menus($menus);
        });
    }

    private function setUpHandlebars()
    {
        if ($this->config["handlebars"] === false) {
            return;
        }

        $this->fileExtension = "tpl";
        if (is_array($this->config["handlebars"]) &&
            array_key_exists("template-extension", $this->config["handlebars"])
        ) {
            $this->fileExtension = $this->config["handlebars"]["template-extension"];
        }

        $this->templateDirectory = "tpl";
        if (is_array($this->config["handlebars"]) &&
            array_key_exists("template-directory", $this->config["handlebars"])
        ) {
            $this->templateDirectory = $this->config["handlebars"]["template-directory"];
        }

        $this->partialsDir = get_template_directory()."/".$this->templateDirectory;
        if (!file_exists($this->partialsDir) && !is_dir($this->partialsDir)) {
            // check for child theme?
            $this->partialsDir = get_stylesheet_directory()."/".$this->templateDirectory;

            if (!file_exists($this->partialsDir) && !is_dir($this->partialsDir)) {
                $this->adminError("Your Handlebars directory is not setup correctly or doesn't exist.");
                return;
            }
            $this->adminError("Your Handlebars directory is not setup correctly or doesn't exist.");
            return;
        }
        $partialsLoader = new FilesystemLoader($this->partialsDir, ["extension" => $this->fileExtension]);

        $this->hb = new Handlebars([
            "loader" => $partialsLoader,
            "partials_loader" => $partialsLoader,
            "enableDataVariables" => true,
        ]);

        // built in helpers
        $helpers = [
            "wp-header" => \ofc\RadThemeEngine::wpHeader(),
            "wp-footer" => \ofc\RadThemeEngine::wpFooter(),
            "wp-title" => \ofc\RadThemeEngine::wpTitle(),
            "body-classes" => \ofc\RadThemeEngine::bodyClasses(),
            "json-encode" => \ofc\RadThemeEngine::jsonEncode(),
            "json-access" => \ofc\RadThemeEngine::jsonAccess(),
            "flex" => \ofc\RadThemeEngine::processFlex(),
            "nl2br" => \ofc\RadThemeEngine::nl2br(),
            "assetURL" => \ofc\RadThemeEngine::assetURL(),
            "assetContents" => \ofc\RadThemeEngine::assetContents(),
        ];
        foreach ($helpers as $name => $callback) {
            $this->hb->addHelper($name, $callback);
        }

        // additional helpers defined in the config file
        if (isset($this->config["handlebars"]["additional-helpers"])) {
            foreach ($this->config["handlebars"]["additional-helpers"] as $name => $callback) {
                $this->hb->addHelper($name, $callback);
            }
        }
    }

    /**
     * render
     * See $this->view()
     *
     * @param string $fileName
     * @param array $data
     * @return string
     */
    public function render(string $fileName, array $data = []) : string
    {
        return $this->view($fileName, $data);
    }

    /**
     * view
     * Invoke handlebar rendering engine for a given template file and data array
     *
     * @param string $fileName
     * @param array $data
     * @return string
     */
    public function view(string $fileName, array $data = []) : string
    {
        $filePath = $this->partialsDir."/".$fileName.".".$this->fileExtension;
        if (!file_exists($filePath)) {
            if ($this->config["debug"] === true) {
                return "<pre>View {$fileName}.{$this->fileExtension} does not exist.\nData:\n"
                    .print_r($data, true)
                    ."</pre>";
            }
            return "";
        }
        return $this->hb->render($fileName, $data);
    }

    /**
     * view
     * Invoke handlebar rendering engine for a given template file and data array
     *
     * @param string $fileName
     * @param array $data
     * @return string
     */
    public function renderTemplate(string $template, array $data = []) : string
    {
        $hb = new Handlebars();
        $hb->addHelper("json-encode", \ofc\RadThemeEngine::jsonEncode());
        return $hb->render($template, $data);
    }

    public function getCurrentPost($fields = [])
    {
        global $post;
        return $this->getPost($post, $fields);
    }

    public function getPost($idOrPost, $fields = [])
    {
        if (is_numeric($idOrPost)) {
            $p = get_post($idOrPost);
        } elseif ($idOrPost instanceof WP_Post) {
            $p = $idOrPost;
        } else {
            return [
                "error" => "You must pass an ID or WP_Post to this method"
            ];
        }

        if ($fields == []) {
            return [
                'post' => $p,
                'meta' => get_post_meta($p->ID),
            ];
        }

        $output = [];
        $categories = [];
        $taxonomies = [];

        foreach ($fields as $key) {
            // handle url/permalink
            if ($key === "url" || $key === "permalink") {
                $output[$key] = get_permalink($p->ID);
                continue;
            }

            // handle post content
            if ($key === "content") {
                $output[$key] = apply_filters('the_content', get_the_content(null, false, $p->ID));
                continue;
            }

            // handle post excerpt
            if ($key === "excerpt") {
                $output[$key] = apply_filters("the_content", get_the_excerpt($p->ID));
                continue;
            }

            // handle adjacent posts
            if ($key === "nextPost") {
                $output[$key] = get_next_post();
                continue;
            }
            if ($key === "previousPost") {
                $output[$key] = get_previous_post();
                continue;
            }

            // handle adjacent post attributes
            if (substr($key, 0, 9) === "nextPost.") {
                $fields = explode(",", substr($key, 9));
                $output[substr($key, 0, 8)] = $this->getPost(get_next_post(), $fields);
                continue;
            }
            if (substr($key, 0, 13) === "previousPost.") {
                $fields = explode(",", substr($key, 13));
                $output[substr($key, 0, 12)] = $this->getPost(get_previous_post(), $fields);
                continue;
            }

            if (substr($key, 0, 7) === "parent.") {
                if ($p->post_parent == 0) {
                    $output[substr($key, 0, 6)] = [];
                    continue;
                }
                $fields = explode(",", substr($key, 7));
                $output[substr($key, 0, 6)] = $this->getPost($p->post_parent, $fields);
                continue;
            }

            // handle meta keys
            if (substr($key, 0, 5) === "meta.") {
                $output[substr($key, 5)] = get_post_meta($p->ID, substr($key, 5), true);
                continue;
            }

            // handle better wordpress fields
            if (substr($key, 0, 4) === "rad.") {
                if (substr($key, -5) === "-JSON") {
                    $key = substr($key, 0, strlen($key) - 5);
                    $output[substr($key, 4)] = json_decode(get_post_meta($p->ID, str_replace("rad.", "rad_", $key), true));
                    continue;
                }
                $output[substr($key, 4)] = get_post_meta($p->ID, str_replace("rad.", "rad_", $key), true);
                continue;
            }

            if (substr($key, 0, 4) === "acf.") {
                $output[substr($key, 4)] = get_field(substr($key, 4), $p->ID);
                continue;
            }

            if (substr($key, 0, 11) === "categories.") {
                if (count($categories) == 0) {
                    $categories = wp_get_post_categories($p->ID, ["fields" => "all"]);
                    $attrs = explode(",", substr($key, 11));

                    foreach ($categories as $cat) {
                        $dataToAppend = [];
                        foreach ($attrs as $key) {
                            $oldKey = $key;
                            if ($key == "id") {
                                $key = "term_id";
                            }
                            if (isset($cat->$key)) {
                                $dataToAppend[$oldKey] = $cat->$key;
                            }
                        }
                        $output["categories"][] = $dataToAppend;
                    }
                }
                continue;
            }

            if (substr($key, 0, 9) === "taxonomy.") {
                $taxonomyKey = $this->parseTaxKey($key);
                $values = explode(",", $this->parseTaxValues($key));
                if (!isset($taxonomies[$taxonomyKey])) {
                    $taxonomies[$taxonomyKey] = $this->getPostTaxonomy($p, $taxonomyKey);
                    $output[$taxonomyKey] = [];
                }

                if (!is_array($taxonomies[$taxonomyKey])) {
                    continue;
                }

                foreach ($taxonomies[$taxonomyKey] as $tax) {
                    $taxonomyDTO = [];
                    foreach ($values as $val) {
                        if ($val === "id" || $val === "term_id") {
                            $taxonomyDTO["id"] = $tax->term_id;
                            continue;
                        }

                        if ($val === "link") {
                            $taxonomyDTO["link"] = get_term_link($tax);
                            continue;
                        }

                        if ($val === "url") {
                            $taxonomyDTO["url"] = get_term_link($tax);
                            continue;
                        }

                        if ($val === "permalink") {
                            $taxonomyDTO["permalink"] = get_term_link($tax);
                            continue;
                        }

                        if ($val === "name") {
                            $taxonomyDTO["name"] = $tax->name;
                            continue;
                        }

                        if ($val === "title") {
                            $taxonomyDTO["title"] = $tax->name;
                            continue;
                        }

                        if ($val === "slug") {
                            $taxonomyDTO["slug"] = $tax->slug;
                            continue;
                        }

                        if ($val === "description") {
                            $taxonomyDTO["description"] = $tax->description;
                            continue;
                        }
                    }
                    $output[$taxonomyKey][] = $taxonomyDTO;
                }
                continue;
            }

            // let's keep our sanity here...
            // the following keys are here for convienance. WP names things so weirdly!
            if ($key === "id") {
                $output[$key] = $p->ID;
                continue;
            }
            if ($key === "title" || $key === "name") {
                $output[$key] = $p->post_title;
                continue;
            }
            if (in_array($key, ["published", "publishedat", "publishedon", "published_at", "published_on"])) {
                $output[$key] = $p->post_date;
                continue;
            }
            if ($key === "thumbnail") {
                $output[$key] = get_the_post_thumbnail_url($p->ID);
                continue;
            }

            $output[$key] = $p->$key;
        }
        return $output;
    }

    public function getPosts($args = [], $fields = [])
    {
        $args = $this->processArgs($args);
        $posts = get_posts($args);

        if ($fields == []) {
            return [
                'posts' => $posts,
            ];
        }

        $output = [];
        foreach ($posts as $p) {
            $output[] = $this->getPost($p, $fields);
        }
        return $output;
    }

    /**
     * getCurrentPosts
     * see getDefaultPosts
     *
     * @deprecated 1.0.15
     *
     * @param array $fields
     * @return array
     */
    public function getCurrentPosts($fields = []) : array
    {
        return $this->getDefaultPosts($fields);
    }

    /**
     * getDefaultPosts
     * get all the posts for the current page as defined by wordpress' weird rules for archive pages
     *
     * @param array $fields
     * @return array
     */
    public function getDefaultPosts($fields = []) : array
    {
        $output = [];
        if (!have_posts()) {
            return $output;
        }
        while (have_posts()) {
            the_post();
            global $post;
            $output[] = $this->getPost($post, $fields);
        }
        return $output;
    }

    public function getTerm($slug, $fields = [])
    {
        $args = [
            'taxonomy' => $slug,
            'hide_empty' => false,
            'suppress_filter' => true,
        ];
        $results = get_terms($args);

        if ($fields == []) {
            return $results;
        }

        $output = [];
        foreach ($results as $term) {
            $append = [];
            foreach ($fields as $key) {
                $oldKey = $key;
                if ($key == "id") {
                    $key = "term_id";
                }
                if ($key == "title") {
                    $key = "name";
                }

                if ($oldKey != $key) {
                    $append[$oldKey] = $term->$key;
                    continue;
                }

                if ($key === "url" || $key === "permalink") {
                    $append[$key] = get_term_link($term);
                    continue;
                }
                $append[$key] = $term->$key;
            }
            $output[] = $append;
        }
        return $output;
    }

    private function processArgs(array $args) : array
    {
        // wordpress default
        $args = array_merge(
            [
                'numberposts' => 5,
                'category' => 0,
                'orderby' => 'date',
                'order' => 'DESC',
                'include' => array(),
                'exclude' => array(),
                'meta_key' => '',
                'meta_value' => '',
                'post_type' => 'post',
                'suppress_filters' => true,
            ],
            $args
        );

        if (isset($args["type"])) {
            $args["post_type"] = $args["type"];
        }

        if (isset($args["title"])) {
            $args["title"] = urldecode($args["title"]);
        }

        if (isset($args["s"])) {
            $args["s"] = urldecode($args["s"]);
        }

        // process custom taxonomy
        $taxQuery = [];
        foreach ($args as $paramKey => $paramValue) {
            if (!(substr($paramKey, 0, 4) === "tax." || substr($paramKey, 0, 9) === "taxonomy.")) {
                continue;
            }

            if (substr($paramKey, 0, 4) === "tax.") {
                $tax = substr($paramKey, 4);
            }

            if (substr($paramKey, 0, 9) === "taxonomy.") {
                $tax = substr($paramKey, 9);
            }

            $field = "slug";
            $values = explode(",", $paramValue);
            $allValuesAreNumeric = false;
            foreach ($values as $v) {
                if (is_numeric($v)) {
                    $allValuesAreNumeric = true;
                }
            }

            if ($allValuesAreNumeric) {
                $field = "term_id";
            }

            $taxQuery[] = [
                "taxonomy" => $tax,
                "field" => $field,
                "terms" => $values,
            ];
        }
        if ($taxQuery != []) {
            $args['tax_query'] = $taxQuery;
        }

        return $args;
    }

    private function parseTaxKey(string $paramater) : string
    {
        $parts = explode(".", $paramater);
        if (count($parts) != 3) {
            return $paramater;
        }
        return $parts[1];
    }

    private function parseTaxValues(string $paramater) : string
    {
        $parts = explode(".", $paramater);
        if (count($parts) != 3) {
            return $paramater;
        }
        return $parts[2];
    }

    public function getPostTaxonomy($post, $taxonomyKey)
    {
        return get_the_terms($post->ID, $taxonomyKey);
    }

    public function setPostMeta($postID, $data)
    {
        foreach ($data as $metaKey => $metaValue) {
            update_post_meta($postID, $metaKey, $metaValue);
        }
    }

    /**
     * add an error to the wordpress backend
     *
     * @param string $message
     */
    private function adminError(string $message)
    {
        $key = $this->stringify($message);
        add_action('admin_notices', function ($messages) use ($message, $key) {
                add_settings_error($key, '', "OFC Site Error: $message", 'error');
                settings_errors($key);
            return $messages;
        });
    }

    public function renderMenu($menuLocation)
    {
        return wp_nav_menu([
            "theme_location" => $menuLocation,
            "echo" => false,
        ]);
    }

    public function menuArray($menuLocation)
    {
        $locations = get_nav_menu_locations();
        $output = [];
        if (!isset($locations[$menuLocation])) {
            return $output;
        }

        foreach (wp_get_nav_menu_items($locations[$menuLocation]) as $menuItem) {
            if ($menuItem->menu_item_parent === "0") {
                $output[$menuItem->ID] = [
                    "id" => $menuItem->ID,
                    "title" => $menuItem->title,
                    "url" => $menuItem->url,
                    "hasChildren" => false,
                    "children" => [],
                ];
                continue;
            }
            $output = $this->addMenuItemToChildren($output, $menuItem);
        }
        return $output;
    }

    private function addMenuItemToChildren(array $items, $childItem)
    {
        foreach ($items as $id => $menuItem) {
            if ($id == $childItem->menu_item_parent) {
                $items[$id]["hasChildren"] = true;
                $items[$id]["children"][$childItem->ID] = [
                    "id" => $childItem->ID,
                    "title" => $childItem->title,
                    "url" => $childItem->url,
                    "hasChildren" => false,
                    "children" => [],
                ];
                return $items;
            }
        }

        foreach ($items as $id => $parent) {
            foreach ($parent["children"] as $childID => $menuItem) {
                $items[$id]["children"] = $this->addMenuItemToChildren($parent["children"], $childItem);
            }
        }
        return $items;
    }

    private function stringify(string $thing) : string
    {
        $bad = [" ", "/"];
        $good = ["_", ""];
        return strtolower(str_replace($bad, $good, $thing));
    }

    public function getAssetURL(string $filename) : string
    {
        if (!file_exists(get_template_directory()."/assets/$filename")) {
            $this->adminError("Requested asset $filename doesn't exist.");
            return "";
        }

        return get_template_directory_uri()."/assets/$filename";
    }

    public function getAssetContents(string $filename) : string
    {
        if (!file_exists(get_template_directory()."/assets/$filename")) {
            return "Asset doesn't exist: ".get_template_directory()."/assets/$filename";
        }

        return file_get_contents(get_template_directory()."/assets/$filename");
    }

    private function generateLabels(string|array $slug) : array
    {
        if (is_array($slug)) {
            return $slug;
        }

        return [
            'name' => $this->humanize($slug, true),
            'singular_name' => $this->humanize($slug, false),
            'menu_name' => $this->humanize($slug, true),
            'all_items' => "All " . $this->humanize($slug, true),
            'add_new' => "Add New {$this->humanize($slug, false)}",
            'add_new_item' => "Add New {$this->humanize($slug, false)}",
            'edit_item' => "Edit {$this->humanize($slug, false)}",
            'new_item' => "New {$this->humanize($slug, false)}",
            'view_item' => "View {$this->humanize($slug, false)}",
            'search_items' => "Search {$this->humanize($slug, true)}",
            'not_found' => "No {$this->humanize($slug, true)} found",
            'not_found_in_trash' => "No {$this->humanize($slug, true)} found in Trash",
            'parent_item_colon' => "Parent {$this->humanize($slug, false)}:",
        ];
    }

    private function humanize(string $word, bool $makePlural = false) : string
    {
        $humanized = ucwords(strtolower(str_replace(['-', '_'], ' ', $word)));

        if (!$makePlural) {
            return $humanized;
        }

        if (strtolower(substr($humanized, strlen($humanized)-1, 1)) == "s") {
            return $humanized . "es";
        }
        if (strtolower(substr($humanized, strlen($humanized)-1, 1)) == "y") {
            return substr($humanized, 0, strlen($humanized)-1) . "ies";
        }
        return $humanized . "s";
    }

    public function getPaginationLinks() : array
    {
        global $wp_query;

        $output = [
            "older" => false,
            "newer" => false,
            "totalPages" => 0,
            "currentPage" => 0,
        ];

        if (is_singular()) {
            return $output;
        }

        $max_num_pages = (int) $wp_query->max_num_pages;
        $paged = get_query_var('paged');
        if (($paged + 1) < $max_num_pages) {
            $output["older"] = next_posts(0, false);
        }

        if ($max_num_pages > 1 && $paged <= $max_num_pages && $paged > 0) {
            $output["newer"] = previous_posts(false);
        }

        $output["totalPages"] = $max_num_pages;
        $output["currentPage"] = $paged;

        return $output;
    }

    private function processCustomFields()
    {
        $templateFields = [];
        $tplFileContents;

        $theme = wp_get_theme();
        $templates = $theme->get_page_templates();
        foreach ($templates as $filename => $templateName) {
            $tplFileContents = file_get_contents(get_theme_file_path($filename));
            $fields = explode("\$fields", $tplFileContents);
            if (count($fields) < 2) {
                continue;
            }
            $fields = explode(";", $fields[1]);

            $fieldArray = eval("use ofc\RadField; return " . preg_replace('/=/', "", $fields[0], 1) . ";");
            $templateFields = [...$templateFields, ...$fieldArray];

            //add template name to first element of name so we can grab it in Util.php
            //it will be removed when processing each field
            array_unshift($templateFields, $templateName);
        }


        // some default admin ajax hooks for field processing
        $existingCPTSlugs = $this->cptSlugs;
        add_action('wp_ajax_rad_theme_engine_related', function () use ($existingCPTSlugs) {
            $args = [
                "type" => array_merge(["page", "post"], $existingCPTSlugs),
                "limit" => 5,
                "order" => "asc",
                "orderby" => "title",
                "s" => $_POST["q"] ?? "",
            ];

            $results = site()->getPosts($args, ["title", "id", "url"]);
            if (count($results) < 1) {
                $results = [
                    [
                        "title" => "No results",
                        "id" => 0,
                        "url" => "#"
                    ]
                ];
            }

            echo json_encode($results);
            wp_die();
        });

        Util::processFieldGroup($templateFields);
    }
}
