<?php /*

 Composr
 Copyright (c) Christopher Graham, 2004-2024

 See docs/LICENSE.md for full licensing information.


 NOTE TO PROGRAMMERS:
   Do not edit this file. If you need to make changes, save your changed file to the appropriate *_custom folder
   **** If you ignore this advice, then your website upgrades (e.g. for bug fixes) will likely kill your changes ****

*/

/**
 * @license    http://opensource.org/licenses/cpal_1.0 Common Public Attribution License
 * @copyright  Christopher Graham
 * @package    themewizard
 */

/**
 * Standard code module initialisation function.
 *
 * @ignore
 */
function init__themewizard()
{
    global $THEMEWIZARD_IMAGES_CACHE, $THEME_DARK_CACHE;
    $THEMEWIZARD_IMAGES_CACHE = [];
    $THEME_DARK_CACHE = [];

    require_code('images');
}

/**
 * Given a source theme name, configure the Theme Wizard for theme generation from it.
 *
 * @param  ID_TEXT $theme The theme name
 * @param  boolean $guess_images_if_needed Whether we suspect the theme might not be well defined
 */
function load_themewizard_params_from_theme(string $theme, bool $guess_images_if_needed = false)
{
    global $THEMEWIZARD_IMAGES_CACHE;
    if (isset($THEMEWIZARD_IMAGES_CACHE[$theme])) {
        return;
    }

    global $THEMEWIZARD_IMAGES, $THEMEWIZARD_IMAGES_NO_WILD;
    $THEMEWIZARD_IMAGES = [''];
    $THEMEWIZARD_IMAGES_NO_WILD = [];

    $hooks = find_all_hook_obs('modules', 'admin_themewizard', 'Hook_admin_themewizard_');
    foreach ($hooks as $ob) {
        $results = $ob->run();
        if ($results === null) {
            continue;
        }
        list($a, $b) = $results;
        $THEMEWIZARD_IMAGES = array_merge($THEMEWIZARD_IMAGES, $a);
        $THEMEWIZARD_IMAGES_NO_WILD = array_merge($THEMEWIZARD_IMAGES_NO_WILD, $b);
    }

    $themewizard_images = get_theme_option('themewizard_images', null, $theme);
    $themewizard_images_no_wild = get_theme_option('themewizard_images_no_wild', null, $theme);

    $autodetect_background_images = (($guess_images_if_needed) && (get_theme_option('themewizard_images', '', $theme) == ''));

    if ($autodetect_background_images) {
        $css_files = themewizard_find_css_sheets($theme) + themewizard_find_css_sheets('default');

        foreach ($css_files as $css_path) {
            $contents = cms_file_get_contents_safe($css_path, FILE_READ_LOCK | FILE_READ_UNIXIFIED_TEXT | FILE_READ_BOM);
            $matches = [];
            $num_matches = preg_match_all('#\{\$IMG[;\#]?,([\w\-]+)\}#', $contents, $matches);
            for ($i = 0; $i < $num_matches; $i++) {
                if ((preg_match('#' . preg_quote($matches[0][$i]) . '[\'"]?\)[^\n]*no-repeat#', $contents) == 0) || (preg_match('#' . preg_quote($matches[0][$i]) . '[\'"]?\)[^\n]*width:\s*\d\d\d+px#', $contents) != 0) || (preg_match('#width:\s*\d\d\d+px;[^\n]*' . preg_quote($matches[0][$i]) . '[\'"]?\)#', $contents) != 0)) {
                    $themewizard_images .= ',' . $matches[1][$i];
                }
            }
        }

        if ($theme != 'default') {
            $myfile = cms_fopen_text_write(get_custom_file_base() . '/themes/' . filter_naughty($theme) . '/theme.ini', false, 'ab');
            fwrite($myfile, 'themewizard_images=' . $themewizard_images . "\n");
            flock($myfile, LOCK_UN);
            fclose($myfile);
        }
    }

    global $THEMEWIZARD_IMAGES, $THEMEWIZARD_IMAGES_NO_WILD;
    $THEMEWIZARD_IMAGES = explode(',', $themewizard_images);
    $THEMEWIZARD_IMAGES_NO_WILD = explode(',', $themewizard_images_no_wild);

    $THEMEWIZARD_IMAGES_CACHE[$theme] = $THEMEWIZARD_IMAGES;
}

/**
 * Find whether a theme is dark.
 *
 * @param  ID_TEXT $theme The theme name
 * @return boolean Whether the theme is dark
 */
function find_theme_dark(string $theme) : bool
{
    global $THEME_DARK_CACHE;
    if (isset($THEME_DARK_CACHE[$theme])) {
        return $THEME_DARK_CACHE[$theme];
    }

    $test = get_theme_option('themewizard_built_with_dark', '');
    if ($test != '') {
        $THEME_DARK_CACHE[$theme] = ($test == '1');
    } else {
        $css_path = get_custom_file_base() . '/themes/' . $theme . '/css_custom/_colours.css';
        if (!is_file($css_path)) {
            $css_path = get_file_base() . '/themes/default/css/_colours.css';
        }
        if (!is_file($css_path)) {
            return false;
        }
        $css_file_contents = cms_file_get_contents_safe($css_path, FILE_READ_LOCK | FILE_READ_UNIXIFIED_TEXT);
        $matches = [];
        if (preg_match('#\{\$THEMEWIZARD_COLOR,\#(.{6}),WB,.*\}#', $css_file_contents, $matches) != 0) {
            $THEME_DARK_CACHE[$theme] = (cms_strtoupper_ascii($matches[1]) != 'FFFFFF');
        } else {
            $THEME_DARK_CACHE[$theme] = false;
        }
    }

    return $THEME_DARK_CACHE[$theme];
}

/**
 * Called by find_theme_image to allow on-the-fly previewing of what Theme Wizard output would look like.
 *
 * @param  ID_TEXT $id The theme image ID
 * @param  boolean $silent_fail Whether to silently fail (i.e. not give out an error message when a theme image cannot be found)
 * @return ?URLPATH URL to image (null: use standard one, this one is not Theme Wizard influenced)
 */
function find_theme_image_themewizard_preview(string $id, bool $silent_fail = false) : ?string
{
    $source_theme = get_param_string('keep_theme_source', 'default');
    $algorithm = get_param_string('keep_theme_algorithm', 'equations');
    if (get_theme_option('supports_themewizard_equations', null, $source_theme) == '0') {
        $algorithm = 'hsv';
    }

    load_themewizard_params_from_theme($source_theme, $algorithm == 'hsv');

    require_code('themes2');
    $seed = get_param_string('keep_theme_seed', find_theme_seed());
    if ($seed == 'random') {
        $_GET['keep_theme_seed'] = str_pad(dechex(mt_rand(0, 255)), 2, '0', STR_PAD_LEFT) . str_pad(dechex(mt_rand(0, 255)), 2, '0', STR_PAD_LEFT) . str_pad(dechex(mt_rand(0, 255)), 2, '0', STR_PAD_LEFT);
        require_lang('themes');
        attach_message(do_lang_tempcode('SEED_IS', escape_html($_GET['keep_theme_seed'])), 'inform');
    }

    $tseed = $_GET['keep_theme_seed'];
    unset($_GET['keep_theme_seed']);
    $test = find_theme_image($id, $silent_fail);
    $_GET['keep_theme_seed'] = $tseed;
    if ($test == '') {
        return null;
    }

    global $THEMEWIZARD_IMAGES, $THEMEWIZARD_IMAGES_NO_WILD;
    if (!in_array($id, $THEMEWIZARD_IMAGES_NO_WILD)) {
        foreach ($THEMEWIZARD_IMAGES as $expression) {
            if (($expression == $id) || ((substr($expression, -1) == '*') && (substr($id, 0, strlen($expression) - 1) . '*' == $expression))) {
                $keep = keep_symbol([]);
                return find_script('themewizard') . '?type=image&show=' . urlencode($id) . $keep;
            }
        }
    }

    return null;
}

/**
 * Check the given details for making a Theme Wizard theme are valid.
 *
 * @param  ID_TEXT $source_theme The theme it's being generated from
 * @param  ID_TEXT $algorithm The algorithm to use
 * @set equations hsv
 * @param  string $seed Seed colour to use
 * @param  boolean $dark Whether it will be a dark theme
 * @param  ?Tempcode $back_url The back URL to change settings (null: do error screen instead)
 * @return ?Tempcode Output (null: none)
 */
function check_themewizard_theme(string $source_theme, string $algorithm, string $seed, bool $dark, ?object $back_url = null) : ?object
{
    if (get_theme_option('enable_themewizard', null, $source_theme) == '0') {
        $error = do_lang_tempcode('THEME_NOT_SUPPORTING_THEMEWIZARD', escape_html(get_theme_option('title', null, $source_theme)), escape_html($source_theme));
        if ($back_url !== null) {
            return redirect_screen(get_screen_title('ERROR_OCCURRED'), $back_url, $error);
        }
        warn_exit($error);
    }

    if (($algorithm != 'hsv') && (get_theme_option('supports_themewizard_equations', null, $source_theme) == '0')) {
        $algorithm = 'hsv';
        attach_message(do_lang_tempcode('EQUATIONS_NOT_SUPPORTED'), 'notice');
    }

    // Check length (6 chars)
    if (strlen($seed) != 6) {
        $error = do_lang_tempcode('INVALID_COLOUR');
        if ($back_url !== null) {
            return redirect_screen(get_screen_title('ERROR_OCCURRED'), $back_url, $error);
        }
        warn_exit($error);
    }

    return null;
}

/**
 * Find theme images in the scope of the Theme Wizard.
 * themewizard_find_theme_images_in_scope should be called at least once before running this function.
 *
 * @param  ID_TEXT $source_theme The theme it's being generated from
 * @param  ?string $seed Seed colour to use (null: don't compare to source theme's seed)
 * @param  ?boolean $dark Whether it will be a dark theme (null: don't compare to source theme's seed)
 * @return array List of theme image codes
 */
function themewizard_find_theme_images_in_scope(string $source_theme, ?string $seed = null, ?bool $dark = null) : array
{
    require_code('themes2');
    if (($seed !== null) && ($dark !== null) && ($seed == find_theme_seed($source_theme)) && ($dark == find_theme_dark($source_theme))) {
        // Nothing to do
        return [];
    }

    global $THEMEWIZARD_IMAGES, $THEMEWIZARD_IMAGES_NO_WILD, $THEME_IMAGES_CACHE;

    require_code('themes2');
    $full_img_set = [];
    foreach ($THEMEWIZARD_IMAGES as $expression) {
        if (substr($expression, -1) == '*') {
            $expression = substr($expression, 0, strlen($expression) - 2); // remove "/*"
            $full_img_set = array_merge($full_img_set, array_keys(get_all_image_codes(get_file_base() . '/themes/' . filter_naughty($source_theme) . '/images', $expression)));
            $full_img_set = array_merge($full_img_set, array_keys(get_all_image_codes(get_file_base() . '/themes/' . filter_naughty($source_theme) . '/images/' . fallback_lang(), $expression)));
        } else {
            $full_img_set[] = $expression;
        }
    }

    // Filter
    $full_img_set_filtered = [];
    foreach ($full_img_set as $image_code) {
        if ((!in_array($image_code, $THEMEWIZARD_IMAGES_NO_WILD)) && (find_theme_image($image_code, true) != '')) {
            $full_img_set_filtered[] = $image_code;
        }
    }
    $full_img_set = $full_img_set_filtered;

    // Find if the image various by language
    $_langs = find_all_langs(true);
    $full_img_set_with_lang_info = [];
    foreach ($full_img_set as $image_code) {
        $urls = [];
        foreach (array_keys($_langs) as $lang) {
            $urls[find_theme_image($image_code, false, false, $source_theme, $lang)] = true;
        }

        $full_img_set_with_lang_info[$image_code] = (count($urls) > 1);
    }

    return $full_img_set_with_lang_info;
}

/**
 * Find CSS files of a theme.
 *
 * @param  ID_TEXT $source_theme The theme it's being generated from
 * @param  ?string $seed Seed colour to use (null: don't compare to source theme's seed)
 * @param  ?boolean $dark Whether it will be a dark theme (null: don't compare to source theme's seed)
 * @param  boolean $in_scope_only Only return if in the scope of the Theme Wizard
 * @return array Map of CSS files from filename to path
 */
function themewizard_find_css_sheets(string $source_theme, ?string $seed = null, ?bool $dark = null, bool $in_scope_only = true) : array
{
    require_code('themes2');
    require_code('files2');

    if (($seed !== null) && ($dark !== null) && ($seed == find_theme_seed($source_theme)) && ($dark == find_theme_dark($source_theme))) {
        // Nothing to do
        return [];
    }

    $sheets = [];

    foreach ([get_custom_file_base(), get_file_base()] as $file_base) {
        foreach (['css_custom', 'css'] as $subdir) {
            $sheets_path = $file_base . '/themes/' . filter_naughty($source_theme) . '/' . $subdir;

            $files = get_directory_contents($sheets_path, $sheets_path, 0, false, true, ['css']);
            foreach ($files as $path) {
                if (!isset($sheets[basename($path)])) {
                    if ((!$in_scope_only) || (strpos(cms_file_get_contents_safe($path, FILE_READ_UNIXIFIED_TEXT | FILE_READ_BOM), '{$THEMEWIZARD_COLOR,') !== false)) {
                        $sheets[basename($path)] = $path;
                    }
                }
            }
        }
    }

    return $sheets;
}

/**
 * Make a theme. Note that this will trigger the AFM.
 *
 * @param  string $theme_name Name of the theme
 * @param  ID_TEXT $source_theme The theme it's being generated from
 * @param  ID_TEXT $algorithm The algorithm to use
 * @set equations hsv
 * @param  string $seed Seed colour to use
 * @param  boolean $dark Whether it will be a dark theme
 * @param  boolean $use_on_all Whether to use the theme immediately
 * @param  boolean $fix_only Whether we are fixing an existing theme
 * @return array List of files created
 */
function generate_themewizard_theme(string $theme_name, string $source_theme, string $algorithm, string $seed, bool $dark, bool $use_on_all = false, bool $fix_only = false) : array
{
    $files_created = [];

    $old_limit = cms_set_time_limit(TIME_LIMIT_EXTEND__SLOW);
    disable_php_memory_limit();

    push_query_limiting(false);

    require_code('themes3');

    if (get_theme_option('supports_themewizard_equations', null, $source_theme) == '0') {
        $algorithm = 'hsv';
    }

    // Find what is in scope
    load_themewizard_params_from_theme($source_theme, $algorithm == 'hsv');
    $theme_images = themewizard_find_theme_images_in_scope($source_theme, $seed, $dark);
    $css_files = themewizard_find_css_sheets($source_theme, $seed, $dark);

    // Create/clone base theme
    if (file_exists(get_custom_file_base() . '/themes/' . $theme_name)) {
        require_code('abstract_file_manager');
        force_have_afm_details(['themes/' . $theme_name . '/css_custom/*', 'themes/' . $theme_name . '/images_custom/*']);
    } elseif (!$fix_only) {
        // The below operations will also activate the AFM
        if ($source_theme == 'default') {
            actual_add_theme($theme_name, false);
        } else {
            actual_copy_theme($source_theme, $theme_name, $theme_images, $css_files, false);
        }
    }

    // Basic colour calculations
    list($colours, $landscape) = calculate_themewizard_css_colours($seed, $dark, $source_theme, $algorithm);

    // Make theme images
    if ((function_exists('imagecolorallocatealpha')) && (!empty($theme_images))) {
        $_langs = find_all_langs(true);
        foreach ($theme_images as $theme_image => $image_is_multi_lang) {
            if ($theme_name != '_temp_') {
                send_http_output_ping();
            }

            foreach (array_keys($_langs) as $lang) {
                if ((!$image_is_multi_lang) && ($lang != fallback_lang())) {
                    continue;
                }

                $image = generate_themewizard_image($seed, $dark, $source_theme, $algorithm, $theme_image, $colours, $landscape, $lang);
                if ($image !== null) {
                    // Derive paths and URLs
                    if ($image_is_multi_lang) {
                        $composite = 'themes/' . filter_naughty($theme_name) . '/images/' . $lang . '/';
                    } else {
                        $composite = 'themes/' . filter_naughty($theme_name) . '/images/';
                    }
                    $ext = (is_string($image) ? 'svg' : 'png');
                    $save_path = get_custom_file_base() . '/' . $composite . $theme_image . '.' . $ext;
                    $save_url = $composite . $theme_image . '.' . $ext;

                    // If already made (as we support auto-resume)
                    if (is_file($save_path)) {
                        continue;
                    }

                    // Make any needed directories
                    if ($image_is_multi_lang) {
                        afm_make_file($composite . '/index.html', '', false);
                    }
                    $pos = strrpos($theme_image, '/');
                    if ($pos !== false) {
                        afm_make_directory($composite . substr($theme_image, 0, $pos), true, true);
                        $parts = explode('/', substr($theme_image, 0, $pos));
                        $build_up = $composite;
                        foreach ($parts as $part) {
                            $build_up .= '/' . $part;
                            afm_make_file($build_up . '/index.html', '', false);
                        }
                    }

                    // Write file
                    if (is_string($image)) {
                        cms_file_put_contents_safe($save_path, $image);
                    } else {
                        cms_imagesave($image, $save_path) or intelligent_write_error($save_path);
                        imagedestroy($image);
                    }

                    // Update database
                    if ($theme_name != '_temp_') {
                        actual_edit_theme_image($theme_image, $theme_name, $lang, $theme_image, $save_url, true);
                    }

                    $files_created[] = $composite . $theme_image . '.' . $ext;
                }
            }
        }
    }

    // Make sheets
    foreach ($css_files as $file => $css_path) {
        $_save_path = 'themes/' . filter_naughty($theme_name) . '/css_custom/' . $file;
        $save_path = get_custom_file_base() . '/' . $_save_path;
        if (file_exists($save_path)) { // If already made (as we support auto-resume)
            continue;
        }

        $output = generate_theme_wizard_css_sheet($file, $css_path, $landscape, $source_theme, $algorithm, $seed);

        require_code('files');
        cms_file_put_contents_safe($save_path, $output, FILE_WRITE_FIX_PERMISSIONS | FILE_WRITE_SYNC_FILE | FILE_WRITE_BOM);

        // .editfrom file
        $c_success = @copy(get_file_base() . '/themes/' . filter_naughty($source_theme) . '/css/' . $file, $save_path . '.editfrom');
        if ($c_success !== false) {
            fix_permissions($save_path . '.editfrom');
            sync_file($save_path . '.editfrom');
        }

        $files_created[] = $_save_path;
    }

    // Write theme.ini file
    $_themeini_path = 'themes/' . filter_naughty($theme_name) . '/theme.ini';
    $themeini_path = get_custom_file_base() . '/' . $_themeini_path;
    if (!is_file($themeini_path)) {
        require_code('files');
        $contents = '';
        if ($theme_name !== null) {
            $contents .= 'title=' . titleify($theme_name) . "\n";
        }
        $contents .= 'themewizard_built_with_source_theme=' . $source_theme . "\n";
        $contents .= 'themewizard_built_with_algorithm=' . $algorithm . "\n";
        $contents .= 'themewizard_built_with_dark=' . ($dark ? '1' : '0') . "\n";
        $contents .= 'themewizard_built_with_seed=' . $seed . "\n";
        $contents .= 'author=' . $GLOBALS['FORUM_DRIVER']->get_username(get_member(), true) . "\n";
        if (($algorithm === 'equations') && (get_theme_option('supports_themewizard_equations', null, $source_theme) === '1')) {
            $contents .= 'supports_themewizard_equations=1' . "\n";
        }
        cms_file_put_contents_safe($themeini_path, $contents, FILE_WRITE_FIX_PERMISSIONS | FILE_WRITE_SYNC_FILE | FILE_WRITE_BOM);

        $files_created[] = $themeini_path;
    }

    // Use it, if requested
    if ($use_on_all) {
        set_live_theme($theme_name);

        require_code('permissions2');
        set_global_category_access('theme', $theme_name);

        erase_persistent_cache();
    }

    cms_set_time_limit($old_limit);

    return $files_created;
}

/**
 * Output a theme component straight to the browser.
 */
function themewizard_script()
{
    if (!addon_installed('themewizard')) {
        warn_exit(do_lang_tempcode('MISSING_ADDON', escape_html('themewizard')));
    }

    header('X-Robots-Tag: noindex');

    require_code('themes2');

    $type = get_param_string('type');
    $source_theme = get_param_string('keep_theme_source', 'default');
    $algorithm = get_param_string('keep_theme_algorithm', 'equations');
    if (get_theme_option('supports_themewizard_equations', null, $source_theme) == '0') {
        $algorithm = 'hsv';
    }
    $show = get_param_string('show');
    $seed = get_param_string('keep_theme_seed', find_theme_seed());
    if ($seed == 'kiddie') {
        $seed = str_pad(dechex(mt_rand(0, 255)), 2, '0', STR_PAD_LEFT) . str_pad(dechex(mt_rand(0, 255)), 2, '0', STR_PAD_LEFT) . str_pad(dechex(mt_rand(0, 255)), 2, '0', STR_PAD_LEFT);
    }
    $_dark = get_param_integer('keep_theme_dark', null);
    $dark = ($_dark === null) ? null : ($_dark == 1);

    switch ($type) {
        case 'css':
            cms_ini_set('ocproducts.xss_detect', '0');
            require_code('tempcode_compiler');
            list($colours, $landscape) = calculate_themewizard_css_colours($seed, $dark, $source_theme, $algorithm);
            foreach (['_base.css', '_colours.css'] as $extra_file) {
                if ($show != $extra_file) { // We need to make sure some extra files are parsed that share THEMEWIZARD_COLOR variables and SET commands that Tempcode will pick up on
                    $css = generate_theme_wizard_css_sheet($extra_file, null, $landscape, $source_theme, $algorithm, $seed);
                    $tpl = template_to_tempcode($css);
                    $tpl->evaluate();
                }
            }
            $css = generate_theme_wizard_css_sheet($show, null, $landscape, $source_theme, $algorithm, $seed);
            header('Content-Type: text/css; charset=' . get_charset());
            $tpl = template_to_tempcode($css);
            $tpl->evaluate_echo();
            break;

        case 'image':
            $image = generate_themewizard_image($seed, $dark, $source_theme, $algorithm, $show);
            if ($image === null) {
                header('Location: ' . escape_header(find_theme_image($show))); // assign_refresh not used, as no UI here
                exit();
            }

            $saveat = cms_tempnam();
            if (is_string($image)) {
                cms_file_put_contents_safe($saveat, $image);

                cms_ob_end_clean();

                header('Content-Type: image/svg+xml');
            } else {
                cms_imagesave($image, $saveat, 'png') or intelligent_write_error($saveat);
                imagedestroy($image);

                cms_ob_end_clean();

                header('Content-Type: image/png');
            }

            readfile($saveat);

            @unlink($saveat);
            break;
    }
}

/**
 * Calculate some component relating to a theme from a colour seed.
 *
 * @param  string $seed Colour seed
 * @param  ?boolean $dark Whether it will be a dark theme (null: autodetect)
 * @param  ID_TEXT $source_theme The theme it's being generated from
 * @param  ID_TEXT $algorithm The algorithm to use
 * @set equations hsv
 * @param  ?array $colours The colour map to use (null: compute)
 * @param  ?array $landscape The computed colour landscape to use (null: compute)
 * @param  ?LANGUAGE_NAME $lang The language to work in (null: default)
 * @return array A pair: extended map of colours, colour expression landscape
 */
function calculate_themewizard_css_colours(string $seed, ?bool &$dark, string $source_theme, string $algorithm, ?array $colours = null, ?array $landscape = null, ?string $lang = null) : array
{
    if (get_theme_option('supports_themewizard_equations', null, $source_theme) == '0') {
        $algorithm = 'hsv';
    }

    if ($seed[0] == '#') {
        $seed = substr($seed, 1);
    }

    $white = 255;
    $black = 0;

    // Strip and decimalize the three colors
    $red = hexdec(substr($seed, 0, 2));
    $green = hexdec(substr($seed, 2, 2));
    $blue = hexdec(substr($seed, 4, 2));

    // Decide the dominant color

    if (($red == $blue) && ($red != $green)) {
        $dominant = 'purple';
    }
    if (($red == $green) && ($red != $blue)) {
        $dominant = 'yellow';
    }
    if (($blue == $green) && ($blue != $red)) {
        $dominant = 'cyan';
    }

    if (($red == $green) && ($red == $blue)) {
        if ($red >= 185) {
            $dominant = 'white';
        }
        if (($red < 185) && ($red >= 100)) {
            $dominant = 'gray';
        }
        if ($red < 100) {
            $dominant = 'black';
        }
    }

    if (($red > $green) && ($red > $blue)) {
        $dominant = 'red';
    }
    if (($green > $red) && ($green > $blue)) {
        $dominant = 'green';
    }
    if (($blue > $red) && ($blue > $green)) {
        $dominant = 'blue';
    }

    // Decide if this is a "light" skin or a "dark" theme
    if ($dark === null) {
        $dark = (intval(round(floatval($red + $green + $blue) / 3.0)) >= 127);
    }
    if ($dark) {
        $light_dark = 'dark';
        $anti_light_dark = 'light';
        $wb = '000000';
        $awb = 'FFFFFF';
    } else {
        $light_dark = 'light';
        $anti_light_dark = 'dark';
        $wb = 'FFFFFF';
        $awb = '000000';
    }

    if (($landscape === null) || ($colours === null)) {
        $colours = [
            // Hints for computation
            'dark' => ($light_dark == 'dark') ? '1' : '0',
            'red' => strval($red),
            'green' => strval($green),
            'blue' => strval($blue),
            'dominant' => $dominant,
            'LD' => $light_dark,
            'DL' => $anti_light_dark,

            // Actual colours
            'seed' => $seed,
            'WB' => $wb,
            'BW' => $awb,
        ];
        if ($algorithm == 'equations') {
            list($colours, $landscape) = _calculate_themewizard_dynamic_css_colours($seed, $dark, $colours, $source_theme);
        } else {
            $landscape = [];
        }
    }

    return [$colours, $landscape];
}

/**
 * Augment an array of CSS colours with colours that are derived actually inside the CSS-sheets.
 *
 * @param  string $seed Colour seed
 * @param  boolean $dark Whether it will be a dark theme
 * @param  array $colours Map of colours
 * @param  ID_TEXT $source_theme The theme it's being generated from
 * @return array A pair: extended map of colours, colour expression landscape
 */
function _calculate_themewizard_dynamic_css_colours(string $seed, bool $dark, array $colours, string $source_theme) : array
{
    require_lang('themes');

    // Initialise landscape
    $landscape = [];
    foreach ($colours as $key => $val) {
        if (preg_match('#^[0-9a-f]{6}$#i', $val) != 0) {
            $landscape[$key] = [
                $key, // Colour name
                null, // Parsed expression
                null, // Full match string
                $val, // Final colour
            ];
        }
    }

    // First we build up our landscape
    $css_files = themewizard_find_css_sheets($source_theme, $seed, $dark);
    foreach ($css_files as $file => $path) {
        $contents = cms_file_get_contents_safe($path, FILE_READ_UNIXIFIED_TEXT | FILE_READ_BOM);

        $matches = [];
        $num_matches = preg_match_all('#\{\$THEMEWIZARD_COLOR,(.*),(.*),(.*)\}#', $contents, $matches);

        for ($i = 0; $i < $num_matches; $i++) {
            // Skip over our little stored hints (not intended for calculation, comes with new seed)
            if (in_array($matches[2][$i], ['seed', 'WB', 'BW'])) {
                continue;
            }

            // A one we're really interested in
            $parsed = parse_themewizard_css_colour_expression($matches[3][$i]);
            if ($parsed !== null) {
                $landscape[] = [
                    $matches[2][$i], // Colour name
                    $parsed, // Parsed expression
                    $matches[0][$i], // Full match string
                    null, // Final colour
                ];
            }
        }
    }

    // Then we resolve our expressions
    $resolved_landscaped = [];
    $safety_count = 0;
    while (!empty($landscape)) {
        foreach ($landscape as $i => $peak) {
            if ($peak[3] === null) {
                $peak[3] = execute_themewizard_css_colour_expression($peak[1], $colours);
            }
            if ($peak[3] !== null) { // We were able to get a result
                $resolved_landscaped[] = $peak;
                unset($landscape[$i]);

                // Then we add to the colours array
                if ($peak[0] != 'wizard') { // 'wizard' is a generic name, so we ignore it
                    $colours[$peak[0]] = $peak[3];
                }
            }
        }
        $safety_count++;
        if ($safety_count == 100) {
            $_landscape = '';
            foreach ($landscape as $x) {
                if ($_landscape != '') {
                    $_landscape .= '; ';
                }
                $_landscape .= $x[2];
            }
            warn_exit(do_lang_tempcode('UNRESOLVABLE_COLOURS', escape_html($_landscape)), false, true);
        }
    }

    return [$colours, $resolved_landscaped];
}

/**
 * Convert a textual CSS colour expression into an expression tree.
 *
 * @param  string $textual Textual expression
 * @return ?array Expression tree (null: not real)
 */
function parse_themewizard_css_colour_expression(string $textual) : ?array
{
    // '*' is inserted after a %, and then % is dropped
    $textual = preg_replace('#(^| )(\d+)%#', '\\1\\2 *', $textual);

    // We're using spaces as token delimiters, so we need to do a trim to clean up, and also put spaces around parentheses
    $textual = trim(str_replace(')', ' )', str_replace('(', '( ', $textual)));

    // Perform inner conversion
    $tokens = explode(' ', $textual);

    $expression = _parse_themewizard_css_colour_expression($tokens);
    return $expression;
}

/**
 * Convert CSS colour tokens into an expression tree.
 *
 * @param  array $tokens Tokens
 * @return ?array Expression tree (null: error)
 *
 * @ignore
 */
function _parse_themewizard_css_colour_expression(array $tokens) : ?array
{
    // We now scan through, structuring into an evaluation-order tree (but not an expression tree  at the level we're operating on)
    // Parentheses
    $new_tokens = [];
    for ($i = 0; $i < count($tokens); $i++) {
        if ($tokens[$i] === '(') {
            // Find matching closing token
            $extra_opened = 0;
            $sub_tokens = [];
            for ($i = $i + 1; $i < count($tokens); $i++) {
                if ($tokens[$i] == '(') {
                    $extra_opened++;
                } elseif (($tokens[$i] == ')') && ($extra_opened > 0)) {
                    $extra_opened--;
                } elseif ($tokens[$i] == ')') {
                    $new_tokens[] = _parse_themewizard_css_colour_expression($sub_tokens);
                    break;
                }
                $sub_tokens[] = $tokens[$i];
            }
        } else {
            $new_tokens[] = $tokens[$i];
        }
    }
    $tokens = $new_tokens;
    // Additions. Each addition is a pivot.
    for ($i = 0; $i < count($tokens); $i++) {
        if ($tokens[$i] === '+') {
            return ['+', _parse_themewizard_css_colour_expression(array_slice($tokens, 0, $i)), _parse_themewizard_css_colour_expression(array_slice($tokens, $i + 1))];
        }
    }

    // Either we have a single token
    if (count($tokens) == 1) {
        return $tokens[0];
    }

    // Or we have a length of more than 3 tokens, in which case we pivot
    if (count($tokens) > 3) {
        return [$tokens[1], $tokens[0], _parse_themewizard_css_colour_expression(array_slice($tokens, 2))];
    }

    // Or we have just 3 tokens, a single operation
    if (!array_key_exists(2, $tokens)) {
        return null;
    }
    return [$tokens[1], $tokens[0], $tokens[2]];
}

/**
 * Execute CSS colour expression.
 *
 * @param  mixed $expression Expression tree (array) OR leaf (string)
 * @param  array $colours Known colours at this point
 * @return ?string RRGGBB colour or possibly just a number (null: answer cannot be computed)
 */
function execute_themewizard_css_colour_expression($expression, array $colours) : ?string
{
    if (!is_array($expression)) {
        if (preg_match('#^[0-9A-Fa-f]{6}$#', $expression) != 0) {
            return $expression;
        }
        if (preg_match('#^\#[0-9A-Fa-f]{6}$#', $expression) != 0) {
            return substr($expression, 1);
        }
        if (preg_match('#^[\+\-]?(?:\d+|\d*\.\d+)$#', $expression) != 0) {
            return $expression;
        }

        foreach ($colours as $colour => $actual_colour) {
            if ($colour == $expression) {
                return $actual_colour;
            }
        }

        return null; // Couldn't find it - we'll have to instruct it to come back to it
    }

    $operation = $expression[0];
    $operand_a = execute_themewizard_css_colour_expression($expression[1], $colours);
    if ($operand_a === null) {
        return null;
    }
    $operand_b = execute_themewizard_css_colour_expression($expression[2], $colours);
    if ($operand_b === null) {
        return null;
    }

    if ($operation[0] == '&') {
        $operand_c = str_replace('%', '', substr($operation, 1));
        $operation = '&';
    }

    switch ($operation) {
        /* These are percentage modifiers */
        case 'sat_to':
            list($h, $s, $v) = rgb_to_hsv($operand_a);
            $result = ($s == 0) ? hsv_to_rgb(floatval($h), floatval($s), floatval(fix_colour(255 * intval($operand_b) / (100.0)))) : hsv_to_rgb(floatval($h), floatval(fix_colour(255 * intval($operand_b) / (100.0))), floatval($v));
            break;

        case 'sat_add':
            list($h, $s, $v) = rgb_to_hsv($operand_a);
            $result = hsv_to_rgb(floatval($h), floatval(fix_colour($s + intval($operand_b))), floatval($v));
            break;

        case 'sat':
            list($h, $s, $v) = rgb_to_hsv($operand_a);
            $result = hsv_to_rgb(floatval($h), floatval(fix_colour($s * intval($operand_b) / (100.0))), floatval($v));
            break;

        case 'val_to':
            list($h, $s, $v) = rgb_to_hsv($operand_a);
            $result = hsv_to_rgb(floatval($h), floatval($s), floatval(fix_colour(255 * intval($operand_b) / (100.0))));
            break;

        case 'val_add':
            list($h, $s, $v) = rgb_to_hsv($operand_a);
            $result = hsv_to_rgb(floatval($h), floatval($s), floatval(fix_colour($v + intval($operand_b))));
            break;

        case 'val':
            list($h, $s, $v) = rgb_to_hsv($operand_a);
            $result = hsv_to_rgb(floatval($h), floatval($s), floatval(fix_colour($v * intval($operand_b) / (100.0))));
            break;

        case 'hue_to':
            list($h, $s, $v) = rgb_to_hsv($operand_a);
            $result = hsv_to_rgb(floatval(fix_colour(255 * intval($operand_b) / (100.0), true)), floatval($s), floatval($v));
            break;

        case 'hue_add':
            list($h, $s, $v) = rgb_to_hsv($operand_a);
            $result = hsv_to_rgb(floatval(fix_colour($h + intval($operand_b), true)), floatval($s), floatval($v));
            break;

        case 'hue':
            list($h, $s, $v) = rgb_to_hsv($operand_a);
            $result = hsv_to_rgb(floatval(fix_colour($h * intval($operand_b) / (100.0), true) % 255), floatval($s), floatval($v));
            break;

        case '&':
            $red = hexdec(substr($operand_a, 0, 2));
            $green = hexdec(substr($operand_a, 2, 2));
            $blue = hexdec(substr($operand_a, 4, 2));
            $fraction = 1.0 - $operand_c / 100.0;

            $red_b = hexdec(substr($operand_b, 0, 2));
            $green_b = hexdec(substr($operand_b, 2, 2));
            $blue_b = hexdec(substr($operand_b, 4, 2));

            $red = intval($fraction * $red + (1 - $fraction) * $red_b);
            $green = intval($fraction * $green + (1 - $fraction) * $green_b);
            $blue = intval($fraction * $blue + (1 - $fraction) * $blue_b);

            $result = str_pad(dechex(fix_colour($red)), 2, '0', STR_PAD_LEFT) . str_pad(dechex(fix_colour($green)), 2, '0', STR_PAD_LEFT) . str_pad(dechex(fix_colour($blue)), 2, '0', STR_PAD_LEFT);
            break;

        case '*':
            $red = intval(round(floatval(hexdec(substr($operand_b, 0, 2)) * intval($operand_a)) / (100.0)));
            $green = intval(round(floatval(hexdec(substr($operand_b, 2, 2)) * intval($operand_a)) / (100.0)));
            $blue = intval(round(floatval(hexdec(substr($operand_b, 4, 2)) * intval($operand_a)) / (100.0)));
            $result = str_pad(dechex($red), 2, '0', STR_PAD_LEFT) . str_pad(dechex($green), 2, '0', STR_PAD_LEFT) . str_pad(dechex($blue), 2, '0', STR_PAD_LEFT);
            break;

        /* These are communicative combinators */

        case '+':
            $red = fix_colour(hexdec(substr($operand_a, 0, 2)) + hexdec(substr($operand_b, 0, 2)));
            $green = fix_colour(hexdec(substr($operand_a, 2, 2)) + hexdec(substr($operand_b, 2, 2)));
            $blue = fix_colour(hexdec(substr($operand_a, 4, 2)) + hexdec(substr($operand_b, 4, 2)));
            $result = str_pad(dechex($red), 2, '0', STR_PAD_LEFT) . str_pad(dechex($green), 2, '0', STR_PAD_LEFT) . str_pad(dechex($blue), 2, '0', STR_PAD_LEFT);
            break;

        case '-':
            $red = fix_colour(hexdec(substr($operand_a, 0, 2)) - hexdec(substr($operand_b, 0, 2)));
            $green = fix_colour(hexdec(substr($operand_a, 2, 2)) - hexdec(substr($operand_b, 2, 2)));
            $blue = fix_colour(hexdec(substr($operand_a, 4, 2)) - hexdec(substr($operand_b, 4, 2)));
            $result = str_pad(dechex($red), 2, '0', STR_PAD_LEFT) . str_pad(dechex($green), 2, '0', STR_PAD_LEFT) . str_pad(dechex($blue), 2, '0', STR_PAD_LEFT);
            break;

        /* These are miscellaneous */

        case 'shift':
            if (intval($operand_b) == 1) {
                $result = substr($operand_a, 4) . substr($operand_a, 0, 4);
            } else {
                $result = substr($operand_a, 2) . substr($operand_a, 0, 2);
            }
            break;

        case 'contrast':
            list($h, $s, $v) = rgb_to_hsv($operand_a);
            $ratio = floatval($operand_b);
            $result = rgb_from_hsv_contrast(floatval($h), floatval($s), floatval($v), $ratio);
            break;
    }

    return $result;
}

/**
 * Make sure a colour component fits within the necessary range (0<=x<256).
 *
 * @param  mixed $x Colour component (float or integer)
 * @param  boolean $hue Whether this is hue (meaning it cycles around)
 * @return integer Constrained colour component
 */
function fix_colour($x, bool $hue = false) : int
{
    if (is_float($x)) {
        $x = intval(round($x));
    }

    if ($hue) {
        while ($x > 255) {
            $x -= 255;
        }
        while ($x < 0) {
            $x += 255;
        }
    } else {
        if ($x > 255) {
            $x = 255;
        }
        if ($x < 0) {
            $x = 0;
        }
    }

    return $x;
}

/**
 * Convert an RGB colour to HSV colour components. Based on publicly distributed code fragments which were themselves based on others: reasonably assumed as public domain.
 *
 * @param  string $rgb RRGGBB colour
 * @return array Triplet of (0-255) components: H, S, V
 */
function rgb_to_hsv(string $rgb) : array
{
    $offset = 0;
    if (strpos($rgb, '#') === 0) {
        $offset = 1;
    }
    $red = hexdec(substr($rgb, $offset, 2));
    $green = hexdec(substr($rgb, $offset + 2, 2));
    $blue = hexdec(substr($rgb, $offset + 4, 2));

    $r = $red / 255.0;
    $g = $green / 255.0;
    $b = $blue / 255.0;
    $h = 0.0;
    $s = 0.0;
    $v = 0.0;
    $min = min($r, $g, $b);
    $max = max($r, $g, $b);
    $delta = $max - $min;

    $v = $max;

    if ($delta == 0.0) {
        $h = 0.0;
        $s = 0.0;
    } else {
        $s = $delta / $max;

        $d_r = ((($max - $r) / 6) + ($delta / 2)) / $delta;
        $d_g = ((($max - $g) / 6) + ($delta / 2)) / $delta;
        $d_b = ((($max - $b) / 6) + ($delta / 2)) / $delta;

        if ($r == $max) {
            $h = $d_b - $d_g;
        } elseif ($g == $max) {
            $h = (1.0 / 3.0) + $d_r - $d_b;
        } else {
            $h = (2.0 / 3.0) + $d_g - $d_r;
        }

        if ($h < 0.0) {
            $h++;
        } elseif ($h > 1.0) {
            $h--;
        }
    }

    return [intval(round($h * 255)), intval(round($s * 255)), intval(round($v * 255))];
}

/**
 * Convert HSV colour components to an RGB colour. Based on publicly distributed code fragments which were themselves based on others: reasonably assumed as public domain.
 *
 * @param  float $h H component
 * @param  float $s S component
 * @param  float $v V component
 * @return string RGB colour
 */
function hsv_to_rgb(float $h, float $s, float $v) : string
{
    $h = 6.0 * $h / 255.0;
    $s = $s / 255.0;
    $v = $v / 255.0;

    if ($s == 0.0) {
        $r = $v;
        $g = $v;
        $b = $v;
    } else {
        $hi = intval(floor($h));
        $f = $h - $hi;
        $p = ($v * (1.0 - $s));
        $q = ($v * (1.0 - ($f * $s)));
        $t = ($v * (1.0 - ((1.0 - $f) * $s)));

        switch ($hi) {
            case 0:
                $r = $v;
                $g = $t;
                $b = $p;
                break;
            case 1:
                $r = $q;
                $g = $v;
                $b = $p;
                break;
            case 2:
                $r = $p;
                $g = $v;
                $b = $t;
                break;
            case 3:
                $r = $p;
                $g = $q;
                $b = $v;
                break;
            case 4:
                $r = $t;
                $g = $p;
                $b = $v;
                break;
            default:
                $r = $v;
                $g = $p;
                $b = $q;
                break;
        }
    }

    return str_pad(dechex(fix_colour(intval(round($r * 255)))), 2, '0', STR_PAD_LEFT) .
           str_pad(dechex(fix_colour(intval(round($g * 255)))), 2, '0', STR_PAD_LEFT) .
           str_pad(dechex(fix_colour(intval(round($b * 255)))), 2, '0', STR_PAD_LEFT);
}

/**
 * Calculate the luminance of a colour.
 *
 * @param  float $r Red
 * @param  float $g Green
 * @param  float $b Blue
 * @return float The luminance of the provided colour
 */
function rgb_luminance(float $r, float $g, float $b) : float
{
    // Convert each colour channel to a 0-1 floating point
    $r /= 255.0;
    $g /= 255.0;
    $b /= 255.0;

    // Linearize each channel
    $r = ($r <= 0.04045) ? ($r / 12.92) : pow(($r + 0.055) / 1.055, 2.4);
    $g = ($g <= 0.04045) ? ($g / 12.92) : pow(($g + 0.055) / 1.055, 2.4);
    $b = ($b <= 0.04045) ? ($b / 12.92) : pow(($b + 0.055) / 1.055, 2.4);

    // Apply weighted luminance
    $r *= 0.2126;
    $g *= 0.7152;
    $b *= 0.0722;

    // Return the sum, which will be a float between 0 and 1.
    return $r + $g + $b;
}

/**
 * Calculate the contrast ratio between two luminances.
 *
 * @param  float $a The first luminance
 * @param  float $b The second luminance
 * @return float The contrast ratio as return_value:1
 */
function contrast_ratio(float $a, float $b) : float
{
    if ($a > $b) {
        return ($a + 0.05) / ($b + 0.05);
    }
    return ($b + 0.05) / ($a + 0.05);
}

/**
 * Find an RGB colour with the specified contrast ratio which contrasts the specified HSV colour.
 *
 * @param  float $h Hue
 * @param  float $s Saturation
 * @param  float $v Value
 * @param  float $ratio The required contrast ratio of the returned RGB colour compared to the provided HSV
 * @return string RGB colour
 */
function rgb_from_hsv_contrast(float $h, float $s, float $v, float $ratio) : string
{
    // Convert to RGB
    $original_colour = hsv_to_rgb($h, $s, $v);
    $original_r = floatval(hexdec(substr($original_colour, 0, 2)));
    $original_g = floatval(hexdec(substr($original_colour, 2, 2)));
    $original_b = floatval(hexdec(substr($original_colour, 4, 2)));

    // Calculate luminance
    $original_luminance = rgb_luminance($original_r, $original_g, $original_b);

    $iterations = 0;
    do {
        $result = _rgb_from_hsv_contrast($h, $s, $v, $original_luminance, $ratio);
        $iterations += 1;

        // Did not get a colour which could satisfy the contrast ratio; shift colours for the next iteration / attempt.
        if ($result === null) {
            $colour = hsv_to_rgb($h, $s, $v);
            $r = floatval(hexdec(substr($colour, 0, 2)));
            $g = floatval(hexdec(substr($colour, 2, 2)));
            $b = floatval(hexdec(substr($colour, 4, 2)));

            $new_colour = str_pad(dechex(fix_colour(intval(round($b * 255)))), 2, '0', STR_PAD_LEFT) .
            str_pad(dechex(fix_colour(intval(round($r * 255)))), 2, '0', STR_PAD_LEFT) .
            str_pad(dechex(fix_colour(intval(round($g * 255)))), 2, '0', STR_PAD_LEFT);

            list($_h, $_s, $_v) = rgb_to_hsv($new_colour);
            $h = floatval($_h);
            $s = floatval($_s);
            $v = floatval($_v);
        }
    } while (($result === null) && ($iterations < 3));

    // Still no compatible colours? Use the black / white method instead.
    if ($result === null) {
        return ($original_luminance >= 0.5) ? hsv_to_rgb(0.0, 0.0, 0.0) : hsv_to_rgb(0.0, 0.0, 255.0);
    }

    return $result;
}

/**
 * Helper for calculating an RGB colour with the specified contrast ratio.
 *
 * @param  float $h Hue
 * @param  float $s Saturation
 * @param  float $v Value
 * @param  float $original_luminance The luminance of the colour being contrasted
 * @param  float $ratio The required contrast ratio of the returned RGB colour compared to the provided HSV
 * @return ?string RGB colour (null: could not find a colour with the provided contrast ratio)
 *
 * @ignore
 */
function _rgb_from_hsv_contrast(float $h, float $s, float $v, float $original_luminance, float $ratio) : ?string
{
    // Ratio should never be 0 or lower; so use black / white as a fallback
    if ($ratio <= 0.0) {
        return ($original_luminance >= 0.5) ? hsv_to_rgb(0.0, 0.0, 0.0) : hsv_to_rgb(0.0, 0.0, 255.0);
    }

    $v2 = $v;
    $s2 = $s;

    $reverse = false;

    // Find a colour until either the ratio is satisfied or our value has been satisfied
    do {
        $colour = hsv_to_rgb($h, $s2, $v2);
        $r = floatval(hexdec(substr($colour, 0, 2)));
        $g = floatval(hexdec(substr($colour, 2, 2)));
        $b = floatval(hexdec(substr($colour, 4, 2)));
        $luminance = rgb_luminance($r, $g, $b);

        // Modify value for our next run in case this colour does not satisfy the ratio
        if ($reverse) {
            $v2 += ($original_luminance >= 0.5) ? 1.0 : -1.0;
        } else {
            $v2 -= ($original_luminance >= 0.5) ? 1.0 : -1.0;
        }

        // If value is at our minimum or maximum, start adjusting saturation instead. If saturation is at min/max, then reverse. If we still have nothing, then exit with null.
        if ($v2 < 0.0) {
            $v2 = 0.0;
            $s2 += 1.0;
            if ($s2 > 255.0) {
                if ($reverse) {
                    return null;
                }
                $v2 = $v;
                $s2 = $s;
                $reverse = true;
            }
        } elseif ($v2 > 255.0) {
            $v2 = 255.0;
            $s2 -= 1.0;
            if ($s2 < 0.0) {
                if ($reverse) {
                    return null;
                }
                $v2 = $v;
                $s2 = $s;
                $reverse = true;
            }
        }
    } while ((contrast_ratio($luminance, $original_luminance) < $ratio));

    return $colour;
}

/**
 * Rewrite a CSS file's code according to a CSS landscape.
 *
 * @param  ID_TEXT $css_file CSS filename of source file
 * @param  ?PATH $css_source_path Where to get the CSS file from (null: search)
 * @param  array $landscape The colour expression landscape which we'll make substitutions using
 * @param  ID_TEXT $source_theme The theme this is being generated from
 * @param  ID_TEXT $algorithm The algorithm to use
 * @set equations hsv
 * @param  ID_TEXT $seed The seed colour
 * @return string The sheet
 */
function generate_theme_wizard_css_sheet(string $css_file, ?string $css_source_path, array $landscape, string $source_theme, string $algorithm, string $seed) : string
{
    $css_sheets = themewizard_find_css_sheets($source_theme, null, null, false) + themewizard_find_css_sheets('default', null, null, false);
    if (!isset($css_sheets[$css_file])) {
        return ''; // Probably a dynamic Theme Wizard call after an addon was removed
    }
    $path = $css_sheets[$css_file];

    $contents = cms_file_get_contents_safe($path, FILE_READ_UNIXIFIED_TEXT | FILE_READ_BOM);

    return themewizard_colours_to_css($contents, $landscape, $source_theme, $algorithm, $seed);
}

/**
 * Rewrite some CSS code according to a CSS landscape.
 *
 * @param  string $contents CSS to apply to
 * @param  array $landscape The colour expression landscape which we'll make substitutions using
 * @param  ID_TEXT $source_theme The theme this is being generated from
 * @param  ID_TEXT $algorithm The algorithm to use
 * @set equations hsv
 * @param  ID_TEXT $seed The seed colour
 * @return string The sheet
 */
function themewizard_colours_to_css(string $contents, array $landscape, string $source_theme, string $algorithm, string $seed) : string
{
    if (running_script('themewizard')) {
        // Pre-parse to see if there are any CSS files we need to hard-include (we can't do a full Tempcode process until the Theme Wizard colour substitutions have occurred)
        //  If we don't do this the included file won't be updated and will be included with the non-altered THEMEWIZARD_COLOR commands at final Tempcode evaluation if running the preview script (as the raw file won't have been changed on disk)
        $matches = [];
        $num_matches = preg_match_all('#/\*\{\+START,INCLUDE,(\w+),\.css,css,default\}\{\+END\}\*/#', $contents, $matches);
        for ($i = 0; $i < $num_matches; $i++) {
            $extra_file = $matches[1][$i] . '.css';

            $css_sheets = themewizard_find_css_sheets($source_theme, null, null, false) + themewizard_find_css_sheets('default', null, null, false);
            if (isset($css_sheets[$extra_file])) {
                $path = $css_sheets[$extra_file];

                $css_include = cms_file_get_contents_safe($path, FILE_READ_UNIXIFIED_TEXT | FILE_READ_BOM);

                $contents = str_replace($matches[0][$i], $css_include, $contents);
            }
        }
    }

    if ($algorithm == 'hsv') {
        list($cms_h, $cms_s, $cms_v) = rgb_to_hsv(find_theme_seed($source_theme));
        list($desired_h, $desired_s, $desired_v) = rgb_to_hsv($seed);
        $hue_dif = $desired_h - $cms_h;
        $sat_dif = 0;//$desired_s-$cms_s;     Actually causes weirdness
        $val_dif = $desired_v - $cms_v;

        $matches = [];
        $num_matches = preg_match_all('#\#([A-Fa-f0-9]{3,6})([^A-Fa-f0-9])#', $contents, $matches);
        for ($i = 0; $i < $num_matches; $i++) {
            list($h, $s, $v) = rgb_to_hsv((strlen($matches[1][$i]) == 3) ? ($matches[1][$i][0] . $matches[1][$i][0] . $matches[1][$i][1] . $matches[1][$i][1] . $matches[1][$i][2] . $matches[1][$i][2]) : $matches[1][$i]);
            $new_colour = hsv_to_rgb(floatval(fix_colour($h + $hue_dif, true)), floatval(fix_colour($s + $sat_dif)), floatval(fix_colour($v + $val_dif)));
            $contents = str_replace([cms_strtolower_ascii($matches[0][$i]), cms_strtoupper_ascii($matches[0][$i])], ['#' . $new_colour . $matches[2][$i], '#' . $new_colour . $matches[2][$i]], $contents);
        }

        return $contents;
    }
    foreach ($landscape as $peak) {
        if ($peak[2] !== null) {
            $from = $peak[2];
            $to = preg_replace('#\{\$THEMEWIZARD_COLOR,\#[\da-fA-F]{6},#', '{$THEMEWIZARD_COLOR,#' . $peak[3] . ',', $peak[2]);
            $contents = str_ireplace($from, $to, $contents);
        } else {
            $to = '{$THEMEWIZARD_COLOR,#' . $peak[3] . ',' . $peak[0] . ',100% ' . $peak[3] . '}';
            $contents = preg_replace('#\{\$THEMEWIZARD_COLOR,\#[\da-fA-F]{6},' . $peak[0] . ',100% [\da-fA-F]{6}\}#i', $to, $contents);
        }
    }

    // Some hints not calculated by equations need separate replacements
    $contents = str_replace('/* Used to initiate equations (although running the Theme Wizard replaces these with what the user chooses - which is how it works) */' . "\n", '', $contents);
    $contents = str_ireplace('/*Theme seed is: ' . find_theme_seed('default') . '*/', '/*Theme seed is: ' . $seed . '*/', $contents);

    return $contents;
}

/**
 * Generate a Theme Wizard image.
 *
 * @param  string $seed Colour seed
 * @param  ?boolean $dark Whether it will be a dark theme (null: autodetect)
 * @param  ID_TEXT $source_theme The theme it's being generated from
 * @param  ID_TEXT $algorithm The algorithm to use
 * @set equations hsv
 * @param  ID_TEXT $show What theme image to generate
 * @param  ?array $colours The colour map to use (null: compute)
 * @param  ?array $landscape The computed colour landscape to use (null: compute)
 * @param  ?LANGUAGE_NAME $lang The language to work in (null: default)
 * @return ?mixed Image resource OR Image string (null: did not happen)
 */
function generate_themewizard_image(string $seed, ?bool $dark, string $source_theme, string $algorithm, string $show, ?array $colours = null, ?array $landscape = null, ?string $lang = null)
{
    list($colours, $landscape) = calculate_themewizard_css_colours($seed, $dark, $source_theme, $algorithm, $colours, $landscape, $lang);

    unset($_GET['keep_theme_seed']);
    $ti = find_theme_image($show, false, true, $source_theme, $lang);
    if ($ti == '') {
        return null;
    }
    $path = get_file_base() . '/' . $ti;
    if (!file_exists($path)) { // File since deleted, we'll revert
        $ti = find_theme_image($show, false, true, $source_theme, $lang, null, true);
        if ($ti == '') {
            return null;
        }
        $path = get_file_base() . '/' . $ti;
    }

    $img = null;

    if (function_exists('imagecolorallocatealpha')) {
        if ($algorithm == 'hsv') {
            $img = re_hue_image($path, $seed, $source_theme, true);
        } else {
            if ($source_theme == 'default') {
                $needed = [];
                foreach ($needed as $colour_needed) {
                    if (!array_key_exists($colour_needed, $colours)) {
                        warn_exit(do_lang_tempcode('UNRESOLVABLE_COLOURS', escape_html($colour_needed)), false, true);
                    }
                }

                if (substr($path, -4) == '.svg') {
                    $img = re_hue_image($path, $seed, $source_theme);
                } else {
                    // This is for raster images to be specially rendered.
                    //  We used to have a lot of logic here, before we moved to .svg

                    switch ($show) {
                        default: // Just change the hue
                            $img = re_hue_image($path, $seed, $source_theme);
                            break;
                    }
                }
            } else {
                $img = re_hue_image($path, $seed, $source_theme);
            }
        }
    }

    return $img;
}

/**
 * Generate a theme image by converting an existing one to a new colour scheme via re-hueing.
 *
 * @param  PATH $path The image path
 * @param  string $seed The colour code of our hue
 * @param  ID_TEXT $source_theme The theme this is being generated from
 * @param  boolean $also_s_and_v Whether to also adjust the S and V components
 * @param  boolean $invert Whether to invert the colours
 * @return mixed The image (resource of text)
 */
function re_hue_image(string $path, string $seed, string $source_theme, bool $also_s_and_v = false, bool $invert = false)
{
    require_code('themes2');
    list($cms_h, $cms_s, $cms_v) = rgb_to_hsv(find_theme_seed($source_theme));
    list($seed_h, $seed_s, $seed_v) = rgb_to_hsv($seed);
    $hue_dif = $seed_h - $cms_h;
    $sat_dif = $seed_s - $cms_s;
    $val_dif = $seed_v - $cms_v;

    if (substr($path, -4) == '.svg') {
       return  _re_hue_image__svg($path, $seed_h, $seed_s, $seed_v, $hue_dif, $sat_dif, $val_dif, $also_s_and_v, $invert);
    }

    return _re_hue_image__raster($path, $seed_h, $seed_s, $seed_v, $hue_dif, $sat_dif, $val_dif, $also_s_and_v, $invert);
}

/**
 * Helper for re_hue_image, SVG images only.
 *
 * @param  PATH $path The image path
 * @param  integer $seed_h Seed hue
 * @param  integer $seed_s Seed saturation
 * @param  integer $seed_v Seed value
 * @param  integer $hue_dif Hue difference
 * @param  integer $sat_dif Saturation difference
 * @param  integer $val_dif Value difference
 * @param  boolean $also_s_and_v Whether to also adjust the S and V components
 * @param  boolean $invert Whether to invert the colours
 * @return string The image
 */
function _re_hue_image__svg(string $path, int $seed_h, int $seed_s, int $seed_v, int $hue_dif, int $sat_dif, int $val_dif, bool $also_s_and_v = false, bool $invert = false) : string
{
    $contents = cms_file_get_contents_safe($path, FILE_READ_UNIXIFIED_TEXT | FILE_READ_BOM);

    $matches = [];
    $num_matches = preg_match_all('/"#([\dA-F]{6})"/i', $contents, $matches);
    $colours = [];
    for ($i = 0; $i < $num_matches; $i++) {
        $colour = $matches[1][$i];
        $colours[$colour] = true;
    }

    foreach (array_keys($colours) as $colour) {
        if (is_integer($colour)) {
            $colour = strval($colour);
        }

        $r = hexdec(substr($colour, 0, 2));
        $g = hexdec(substr($colour, 2, 2));
        $b = hexdec(substr($colour, 4, 2));

        list($new_colour_r, $new_colour_g, $new_colour_b) = _re_hue_image_colour($seed_h, $seed_s, $seed_v, $hue_dif, $sat_dif, $val_dif, $r, $g, $b);

        $new_colour = str_pad(dechex($new_colour_r), 2, '0', STR_PAD_LEFT) . str_pad(dechex($new_colour_g), 2, '0', STR_PAD_LEFT) . str_pad(dechex($new_colour_b), 2, '0', STR_PAD_LEFT);

        $contents = str_replace('"#' . $colour . '"', '"#' . $new_colour . '"', $contents);
    }

    return $contents;
}

/**
 * Helper for re_hue_image, raster images only.
 *
 * @param  PATH $path The image path
 * @param  integer $seed_h Seed hue
 * @param  integer $seed_s Seed saturation
 * @param  integer $seed_v Seed value
 * @param  integer $hue_dif Hue difference
 * @param  integer $sat_dif Saturation difference
 * @param  integer $val_dif Value difference
 * @param  boolean $also_s_and_v Whether to also adjust the S and V components
 * @param  boolean $invert Whether to invert the colours
 * @return resource The image
 */
function _re_hue_image__raster(string $path, int $seed_h, int $seed_s, int $seed_v, int $hue_dif, int $sat_dif, int $val_dif, bool $also_s_and_v = false, bool $invert = false)
{
    $image = cms_imagecreatefrom($path);
    if ($image === false) {
        warn_exit(do_lang_tempcode('CORRUPT_FILE', escape_html($path)), false, true);
    }

    imagepalettetotruecolor($image);

    $width = imagesx($image);
    $height = imagesy($image);
    imagealphablending($image, false);
    imagesavealpha($image, true);

    for ($y = 0; $y < $height; $y++) {
        for ($x = 0; $x < $width; $x++) {
            $_existing_colour = imagecolorat($image, $x, $y);
            $existing_colour = imagecolorsforindex($image, $_existing_colour);

            $r = $existing_colour['red'];
            $g = $existing_colour['green'];
            $b = $existing_colour['blue'];
            $a = $existing_colour['alpha'];

            list($new_colour_r, $new_colour_g, $new_colour_b) = _re_hue_image_colour($seed_h, $seed_s, $seed_v, $hue_dif, $sat_dif, $val_dif, $r, $g, $b);

            if (function_exists('imagecolorallocatealpha')) {
                $target_colour = imagecolorallocatealpha($image, $new_colour_r, $new_colour_g, $new_colour_b, $a);
            } else {
                $target_colour = imagecolorallocate($image, $new_colour_r, $new_colour_g, $new_colour_b);
            }
            imagesetpixel($image, $x, $y, $target_colour);
        }
    }

    return $image;
}

/**
 * Update a colour towards a new seed colour, using the given $*_dif components.
 *
 * @param  integer $seed_h Seed hue
 * @param  integer $seed_s Seed saturation
 * @param  integer $seed_v Seed value
 * @param  integer $hue_dif Hue difference
 * @param  integer $sat_dif Saturation difference
 * @param  integer $val_dif Value difference
 * @param  integer $r Colour red value
 * @param  integer $g Colour green value
 * @param  integer $b Colour blue value
 * @param  boolean $also_s_and_v Whether to also adjust the S and V components
 * @param  boolean $invert Whether to invert the colours
 * @return array New RGB colour components
 */
function _re_hue_image_colour(int $seed_h, int $seed_s, int $seed_v, int $hue_dif, int $sat_dif, int $val_dif, int $r, int $g, int $b, bool $also_s_and_v = false, bool $invert = false) : array
{
    list($h, $s, $v) = rgb_to_hsv(str_pad(dechex($r), 2, '0', STR_PAD_LEFT) . str_pad(dechex($g), 2, '0', STR_PAD_LEFT) . str_pad(dechex($b), 2, '0', STR_PAD_LEFT));
    if ($invert) {
        $v = 255 - $v;
        $v = min($v * 3, 255); // Because it's harder to see deviations of black
    }
    if ($seed_s < 10) {
        $s = $seed_s; // To stop red colours for gray-scale images
    }
    if ($also_s_and_v) {
        $sat_dif = 0; // Actually causes weirdness
        $result = hsv_to_rgb(floatval(fix_colour($h + $hue_dif, true)), floatval(fix_colour($s + $sat_dif)), floatval(fix_colour($v + $val_dif)));
    } else {
        $result = hsv_to_rgb(floatval(fix_colour($h + $hue_dif, true)), floatval($s), floatval($v));
    }

    $new_colour_r = hexdec(substr($result, 0, 2));
    $new_colour_g = hexdec(substr($result, 2, 2));
    $new_colour_b = hexdec(substr($result, 4, 2));

    return [$new_colour_r, $new_colour_g, $new_colour_b];
}

/**
 * Generate a theme image by converting an existing one to a new colour scheme via intelligent blending correlation.
 *
 * @param  mixed $path The image path OR a preloaded GD image resource
 * @param  string $colour_a_orig The colour code of what we have as our "minor" colour (often a border colour)
 * @param  string $colour_a_new The colour code of what we want as our "minor" colour (often a border colour)
 * @param  string $colour_b1_orig The colour code of what we have as our first major colour (often the only major colour)
 * @param  string $colour_b1_new The colour code of what we want as our first major colour (often the only major colour)
 * @param  ?string $colour_b2_orig The colour code of what we have as our second major colour (the gradient target, at the bottom/right of the image) (null: not gradiented)
 * @param  ?string $colour_b2_new The colour code of what we want as our second major colour (the gradient target, at the bottom/right of the image) (null: not gradiented)
 * @param  string $gradient_direction The directional code for the gradient
 * @set vertical horizontal
 * @param  ?array $pixel_x_start_array An array that is used to limit where we do our conversion on. It specifies, for each y-offset, the x-offset we start from (null: no such limitation)
 * @param  integer $gradient_offset What the gradient assumed start-position will be offset by (in the gradient direction)
 * @param  boolean $end_array Whether the pixel_x_start array is actually an end array
 * @return resource The image
 */
function generate_recoloured_image($path, string $colour_a_orig, string $colour_a_new, string $colour_b1_orig, string $colour_b1_new, ?string $colour_b2_orig = null, ?string $colour_b2_new = null, string $gradient_direction = 'vertical', ?array $pixel_x_start_array = null, int $gradient_offset = 0, bool $end_array = false)
{
    /*$colour_a_new = $colour_a_orig;  For testing: a null conversion
    $colour_b1_new = $colour_b1_orig;
    $colour_b2_new = $colour_b2_orig;*/

    $colour_a_orig = str_replace('#', '', $colour_a_orig);
    $colour_b1_orig = str_replace('#', '', $colour_b1_orig);
    if ($colour_b2_new !== null) {
        $colour_b2_orig = str_replace('#', '', $colour_b2_orig);
    }
    $colour_a_new = str_replace('#', '', $colour_a_new);
    $colour_b1_new = str_replace('#', '', $colour_b1_new);
    if ($colour_b2_new !== null) {
        $colour_b2_new = str_replace('#', '', $colour_b2_new);
    }
    $colour_a_orig_r = hexdec(substr($colour_a_orig, 0, 2));
    $colour_a_orig_g = hexdec(substr($colour_a_orig, 2, 2));
    $colour_a_orig_b = hexdec(substr($colour_a_orig, 4, 2));
    $colour_a_new_r = hexdec(substr($colour_a_new, 0, 2));
    $colour_a_new_g = hexdec(substr($colour_a_new, 2, 2));
    $colour_a_new_b = hexdec(substr($colour_a_new, 4, 2));
    $colour_b1_orig_r = hexdec(substr($colour_b1_orig, 0, 2));
    $colour_b1_orig_g = hexdec(substr($colour_b1_orig, 2, 2));
    $colour_b1_orig_b = hexdec(substr($colour_b1_orig, 4, 2));
    $colour_b1_new_r = hexdec(substr($colour_b1_new, 0, 2));
    $colour_b1_new_g = hexdec(substr($colour_b1_new, 2, 2));
    $colour_b1_new_b = hexdec(substr($colour_b1_new, 4, 2));
    if ($colour_b2_new !== null) {
        $colour_b2_orig_r = hexdec(substr($colour_b2_orig, 0, 2));
        $colour_b2_orig_g = hexdec(substr($colour_b2_orig, 2, 2));
        $colour_b2_orig_b = hexdec(substr($colour_b2_orig, 4, 2));
        $colour_b2_new_r = hexdec(substr($colour_b2_new, 0, 2));
        $colour_b2_new_g = hexdec(substr($colour_b2_new, 2, 2));
        $colour_b2_new_b = hexdec(substr($colour_b2_new, 4, 2));
    }

    if (is_string($path)) {
        $_image = cms_imagecreatefrom($path);
        if ($_image === false) {
            warn_exit(do_lang_tempcode('CORRUPT_FILE', escape_html($path)), false, true);
        }
    } else {
        $image = $path;
    }
    imagepalettetotruecolor($image);
    $width = imagesx($image);
    $height = imagesy($image);

    if ($colour_b2_new === null) {
        $colour_b_orig_r = $colour_b1_orig_r;
        $colour_b_orig_g = $colour_b1_orig_g;
        $colour_b_orig_b = $colour_b1_orig_b;
        $colour_b_orig = $colour_b1_orig;
        $colour_b_new_r = $colour_b1_new_r;
        $colour_b_new_g = $colour_b1_new_g;
        $colour_b_new_b = $colour_b1_new_b;
        $colour_b_new = $colour_b1_new;
    }

    $gh = floatval($height - $gradient_offset);
    $gw = floatval($width - $gradient_offset);

    // Protect from a divide by zero, if images tampered with
    if ($gh == 0.0) {
        return $image;
    }
    if ($gw == 0.0) {
        return $image;
    }

    $vertical = ($gradient_direction == 'vertical');
    $horizontal = ($gradient_direction == 'horizontal');

    for ($y = 0; $y < $height; $y++) {
        $x = 0;
        $end = $width;
        if ($end_array) {
            if (($pixel_x_start_array !== null) && (array_key_exists($y, $pixel_x_start_array))) {
                $end = min($width, $pixel_x_start_array[$y]);
            } else {
                $end = $width;
            }
        } else {
            if (($pixel_x_start_array !== null) && (array_key_exists($y, $pixel_x_start_array))) {
                $x = $pixel_x_start_array[$y];
            }
        }
        for (; $x < $end; $x++) {
            $_existing_colour = imagecolorat($image, $x, $y);
            $existing_colour = imagecolorsforindex($image, $_existing_colour);

            if ($colour_b2_new !== null) {
                if ($vertical) {
                    $ratio = floatval($y - $gradient_offset) / $gh;
                } elseif ($horizontal) {
                    $ratio = floatval($x - $gradient_offset) / $gw;
                }
                $colour_b_orig_r = intval($colour_b1_orig_r + ($ratio * ($colour_b2_orig_r - $colour_b1_orig_r)));
                $colour_b_orig_g = intval($colour_b1_orig_g + ($ratio * ($colour_b2_orig_g - $colour_b1_orig_g)));
                $colour_b_orig_b = intval($colour_b1_orig_b + ($ratio * ($colour_b2_orig_b - $colour_b1_orig_b)));
                $colour_b_new_r = intval($colour_b1_new_r + ($ratio * ($colour_b2_new_r - $colour_b1_new_r)));
                $colour_b_new_g = intval($colour_b1_new_g + ($ratio * ($colour_b2_new_g - $colour_b1_new_g)));
                $colour_b_new_b = intval($colour_b1_new_b + ($ratio * ($colour_b2_new_b - $colour_b1_new_b)));
            }

            $existing_colour_r = $existing_colour['red'];
            $existing_colour_g = $existing_colour['green'];
            $existing_colour_b = $existing_colour['blue'];
            $existing_colour_a = $existing_colour['alpha'];

            $scale_r = null;
            $scale_g = null;
            $scale_b = null;
            $scale_count = 0;
            if ($colour_a_orig_r != $colour_b_orig_r) {
                $scale_r = ($colour_a_orig_r - $colour_b_orig_r == 0) ? 0.0 : (floatval($existing_colour_r - $colour_b_orig_r) / floatval($colour_a_orig_r - $colour_b_orig_r));
                $scale_count++;
            }
            if ($colour_a_orig_r != $colour_b_orig_r) {
                $scale_g = ($colour_a_orig_g - $colour_b_orig_g == 0) ? 0.0 : (floatval($existing_colour_g - $colour_b_orig_g) / floatval($colour_a_orig_g - $colour_b_orig_g));
                $scale_count++;
            }
            if ($colour_a_orig_r != $colour_b_orig_r) {
                $scale_b = ($colour_a_orig_b - $colour_b_orig_b == 0) ? 0.0 : (floatval($existing_colour_b - $colour_b_orig_b) / floatval($colour_a_orig_b - $colour_b_orig_b));
                $scale_count++;
            }
            if ($scale_count == 0) { // Impossible to calculate
                $scale = 0.5;
            } else {
                $scale = ($scale_r + $scale_g + $scale_b) / floatval($scale_count);
            }

            $new_colour_r = fix_colour(intval(round(floatval($colour_a_new_r) * $scale + floatval($colour_b_new_r) * (1.0 - $scale))));
            $new_colour_g = fix_colour(intval(round(floatval($colour_a_new_g) * $scale + floatval($colour_b_new_g) * (1.0 - $scale))));
            $new_colour_b = fix_colour(intval(round(floatval($colour_a_new_b) * $scale + floatval($colour_b_new_b) * (1.0 - $scale))));

            if (function_exists('imagecolorallocatealpha')) {
                $target_colour = imagecolorallocatealpha($image, $new_colour_r, $new_colour_g, $new_colour_b, $existing_colour_a);
            } else {
                $target_colour = imagecolorallocate($image, $new_colour_r, $new_colour_g, $new_colour_b);
            }
            imagesetpixel($image, $x, $y, $target_colour);
        }
    }

    return $image;
}

/**
 * Generate a logo from the template.
 *
 * @param  string $name The site name
 * @param  ?string $font_choice The font name (in data/fonts) (null: default)
 * @param  ?string $colour The text colour; may be overridden for certain logos (null: default)
 * @param  string $logo_theme_image The logo theme image
 * @param  string $background_theme_image The background theme image. Not used for 'small' and 'small_white' $logo_type.
 * @param  boolean $raw Whether to output the logo to the browser, destroy then image, and exit the script (i.e. never returns)
 * @param  ?string $theme The theme to use the logo template from (null: default root zone theme)
 * @param  string $logo_type Logo type/size to generate. 'large' is for the bigger logo with background image, 'standalone' crops the background a bit, for use in e-mails etc., 'small' is the transparent logo without background, used for the small header type, and 'small_white' is used when the small header has a dark background
 * @return resource The image resource
 */
function generate_logo(string $name, ?string $font_choice = null, ?string $colour = null, string $logo_theme_image = 'logo/default_logos/logo1', string $background_theme_image = 'logo/default_backgrounds/banner1', bool $raw = false, ?string $theme = null, string $logo_type = 'large')
{
    if (!headers_sent()) {
        header('X-Robots-Tag: noindex');
    }

    require_code('character_sets');
    require_code('files');
    require_code('themes2');

    if ($theme === null) {
        $theme = $GLOBALS['SITE_DB']->query_select_value('zones', 'zone_theme', ['zone_name' => '']);
        if (($theme == '') || ($theme == '-1')) {
            $theme = 'default';
        }
    }

    if (!in_array($logo_type, ['large', 'standalone', 'small', 'small_white'])) {
        // Invalid value for provided for `$logo_type`
        warn_exit(do_lang_tempcode('INTERNAL_ERROR'));
    }

    if ($colour === null) {
        $colour = get_theme_option('header_classic_text_colour', null, $theme);
    }

    if (substr($colour, 0, 1) == '#') {
        $colour = substr($colour, 1);
    }

    $logowizard_details = [
        'logo_x_offset' => get_theme_option('logo_x_offset'),
        'logo_y_offset' => get_theme_option('logo_y_offset'),
        'site_name_split' => get_theme_option('site_name_split'),
        'site_name_split_gap' => get_theme_option('site_name_split_gap'),
        'site_name_font_size_small' => get_theme_option('site_name_font_size_small'),
        'site_name_font_size' => get_theme_option('site_name_font_size'),
        'site_name_font_size_small_non_ttf' => get_theme_option('site_name_font_size_small_non_ttf'),
        // ^ Numeric font identifier: "Can be 1, 2, 3, 4, 5 for built-in fonts in latin2 encoding (where higher numbers corresponding to larger fonts) or any of your own font identifiers registered with imageloadfont()."
        'site_name_font_size_nonttf' => get_theme_option('site_name_font_size_nonttf'),
        // ^ Numeric font identifier: "Can be 1, 2, 3, 4, 5 for built-in fonts in latin2 encoding (where higher numbers corresponding to larger fonts) or any of your own font identifiers registered with imageloadfont()."
        'site_name_x_offset' => get_theme_option('site_name_x_offset'),
        'site_name_y_offset' => get_theme_option('site_name_y_offset'),
        'site_name_y_offset_small' => get_theme_option('site_name_y_offset_small'),
    ];

    // Find font details
    require_code('fonts');
    $font_path = find_font_path($font_choice);
    if (!has_ttf()) {
        $font = intval($logowizard_details['site_name_font_size_small_non_ttf']);
        $font_width = imagefontwidth($font) * strlen($name);
        $font_height = imagefontheight($font);
    } else {
        list(, , $font_width, , , , , $font_height) = imagettfbbox(26.0, 0.0, $font_path, convert_to_html_encoding($name));
        $font_height = max($font_height, -$font_height);
    }

    // If we're doing a white logo, check if we might have a monochrome logo symbol
    if (($logo_type === 'small_white') && (preg_match('#^logo/default_logos/logo(1|2|3|4|5|6|7|8|9|10|11|12)\z#', $logo_theme_image) > 0)) {
        $logo_theme_image = str_replace('logo/default_logos/', 'logo/default_logos_monochrome/', $logo_theme_image);
    }

    $im_logo = _generate_logo_get_image($logo_theme_image, $theme);

    if ($logo_type === 'large') {
        $im_default_logo = _generate_logo_get_image('logo/-logo', $theme);
        $im_canvas = imagecreatetruecolor(imagesx($im_default_logo), imagesy($im_default_logo));

        // Make the canvas transparent
        imagefill($im_canvas, 0, 0, imagecolorallocatealpha($im_canvas, 0, 0, 0, 127));

        imagedestroy($im_default_logo);
    } elseif ($logo_type === 'standalone') {
        $im_background = _generate_logo_get_image($background_theme_image, $theme);
        // Based on 'background' image, but must be the size of 'standalone' image...
        $im_standalone = _generate_logo_get_image('logo/standalone_logo', $theme);
        $im_canvas = imagecreatetruecolor(imagesx($im_standalone), imagesy($im_standalone));
        imagecopy($im_canvas, $im_background, 0, 0, 0, 0, imagesx($im_standalone), imagesy($im_standalone));
        imagedestroy($im_background);
        imagedestroy($im_standalone);
    } elseif (($logo_type === 'small') || ($logo_type === 'small_white')) {
        $small_logo_width = imagesx($im_logo) + $font_width + 20;
        $small_logo_height = imagesy($im_logo);
        $im_canvas = imagecreatetruecolor($small_logo_width, $small_logo_height);

        // Make the canvas transparent
        imagefill($im_canvas, 0, 0, imagecolorallocatealpha($im_canvas, 0, 0, 0, 127));

        // Vertically center the text
        $logowizard_details['site_name_y_offset'] = ($small_logo_height / 2) - ($font_height / 2);

        if ($logo_type === 'small_white') {
            // Make the logo icon/image white:
            $im_logo_width = imagesx($im_logo);
            $im_logo_height = imagesy($im_logo);
            $white_colors_by_alpha = [];
            for ($x = 0; $x < $im_logo_width; $x++) {
                for ($y = 0; $y < $im_logo_height; $y++) {
                    $alpha = imagecolorat($im_logo, $x, $y) >> 24;

                    if ($alpha < 127) { // 127 = fully transparent
                        if (!isset($white_colors_by_alpha[$alpha])) {
                            $white_colors_by_alpha[$alpha] = imagecolorallocatealpha($im_logo, 255, 255, 255, $alpha);
                        }

                        imagesetpixel($im_logo, $x, $y, $white_colors_by_alpha[$alpha]);
                    }
                }
            }

            $colour = 'FFFFFF';
        }
    }

    imagealphablending($im_canvas, true);

    // Add logo onto the canvas
    imagecopy($im_canvas, $im_logo, intval($logowizard_details['logo_x_offset']), intval($logowizard_details['logo_y_offset']), 0, 0, imagesx($im_logo), imagesy($im_logo));
    imagedestroy($im_logo);

    // Set user configured color
    $text_colour = imagecolorallocate($im_canvas, hexdec(substr($colour, 0, 2)), hexdec(substr($colour, 2, 2)), hexdec(substr($colour, 4, 2)));

    if ($logo_type !== 'small_white') {
        // Override user configured color with $THEMEWIZARD_COLOR "box_title_background" if available
        if (file_exists(get_custom_file_base() . '/themes/' . $theme . '/css_custom/_colours.css')) {
            $css_file = cms_file_get_contents_safe(get_custom_file_base() . '/themes/' . $theme . '/css_custom/_colours.css', FILE_READ_LOCK | FILE_READ_UNIXIFIED_TEXT);
        } else {
            $css_file = cms_file_get_contents_safe(get_file_base() . '/themes/default/css/_colours.css', FILE_READ_LOCK | FILE_READ_UNIXIFIED_TEXT);
        }
        $matches = [];
        if (preg_match('#\{\$THEMEWIZARD_COLOR,\#([a-f0-9][a-f0-9])([a-f0-9][a-f0-9])([a-f0-9][a-f0-9]),site_name_text_color,#i', $css_file, $matches) != 0) {
            $text_colour = imagecolorallocate($im_canvas, hexdec($matches[1]), hexdec($matches[2]), hexdec($matches[3]));
        }
    }

    // Write text onto the canvas
    $do = [];
    if (($font_width > intval($logowizard_details['site_name_split'])) && (strpos($name, ' ') !== false)) { // Split in two
        if (has_ttf()) {
            list(, , , , , , , $small_font_height) = imagettfbbox(floatval($logowizard_details['site_name_font_size_small']), 0.0, $font_path, convert_to_html_encoding($name));
            $small_font_height = max($small_font_height, -$small_font_height);
        }
        $bits = explode(' ', $name);
        $text_line_1 = '';
        $text_line_2 = '';
        foreach ($bits as $bit) {
            if (strlen($text_line_1) < intval(round(floatval(strlen($name)) / 2.0))) {
                $text_line_1 .= $bit . ' ';
            } else {
                $text_line_2 .= $bit . ' ';
            }
        }
        $do[] = [$text_line_1, intval($logowizard_details['site_name_x_offset']), intval($logowizard_details['site_name_y_offset_small']) + $small_font_height, intval($logowizard_details['site_name_font_size_small']), $font_path, $text_colour];
        $do[] = [$text_line_2, intval($logowizard_details['site_name_x_offset']), intval($logowizard_details['site_name_y_offset_small']) + $small_font_height * 2 + intval($logowizard_details['site_name_split_gap']), intval($logowizard_details['site_name_font_size_small']), $font_path, $text_colour];
    } elseif ($font_width > intval($logowizard_details['site_name_split'])) { // Smaller font
        if (has_ttf()) {
            list(, , , , , , , $font_height) = imagettfbbox(floatval($logowizard_details['site_name_font_size_small']), 0.0, $font_path, convert_to_html_encoding($name));
            $font_height = max($font_height, -$font_height);
        }
        $do[] = [$name, intval($logowizard_details['site_name_x_offset']), intval($logowizard_details['site_name_y_offset']) + $font_height, intval($logowizard_details['site_name_font_size_small']), $font_path, $text_colour];
    } else { // Show normally
        $do[] = [$name, intval($logowizard_details['site_name_x_offset']), intval($logowizard_details['site_name_y_offset']) + $font_height, floatval($logowizard_details['site_name_font_size']), $font_path, $text_colour];
    }
    foreach ($do as $i => $doing) {
        if (has_ttf()) {
            imagettftext($im_canvas, floatval($doing[3]), 0.0, $doing[1], $doing[2], $doing[5], $doing[4], convert_to_html_encoding($doing[0]));
        } else {
            // @ needed for bizarre reasons due to type juggling in PHP (brought up by ocProducts PHP only)
            @imagestring($im_canvas, ($doing[3] == intval($logowizard_details['site_name_font_size_small'])) ? intval($logowizard_details['site_name_font_size_nonttf']) : $font, $doing[1], $doing[2] - 11, $doing[0], $doing[5]);
        }
    }

    imagesavealpha($im_canvas, true);
    if ($raw) { // Output direct?
        header('Content-Type: image/png');
        //header('Content-Disposition: attachment; filename="-logo.png"');

        if ($_SERVER['REQUEST_METHOD'] == 'HEAD') {
            return '';
        }

        imagepng($im_canvas);
        imagedestroy($im_canvas);

        exit();
    }

    return $im_canvas;
}


/**
 * Generate a PHP GD resource for an image.
 *
 * @param  string $image_codename The image codename
 * @param  string $theme The theme name
 * @return resource The image resource
 */
function _generate_logo_get_image(string $image_codename, string $theme)
{
    $url = find_theme_image($image_codename, false, false, $theme, null, null, true);
    $file_path_stub = convert_url_to_path($url);
    if ($file_path_stub !== null) {
        if (!file_exists($file_path_stub)) {
            // Exceptional situation. Maybe theme got corrupted?
            $file_path_stub = get_file_base() . '/themes/default/images/EN/logo/' . filter_naughty($image_codename) . '.png';
        }
        $data = cms_file_get_contents_safe($file_path_stub, FILE_READ_LOCK);
    } else {
        $data = http_get_contents($url);
    }
    $im = cms_imagecreatefromstring($data, get_file_extension($url));
    if ($im === false) {
        warn_exit(do_lang_tempcode('CORRUPT_FILE', escape_html($url)), false, true);
    }
    return $im;
}
