<?php

$cms["modules"]["template.mod.php"] = array(
    "name"        => "template_module_name",
    "description" => "template_module_description",
    "files"       => array(
        ".cms/mod/template.mod.php",
        ".cms/css/template.less",
        ".cms/css/template.css",
        ".cms/js/template.js",
        ".cms/js/template_less.js",
        ".cms/lang/ru_RU.UTF-8/template.mod.php",
        ".cms/lang/en_US.UTF-8/template.mod.php",
        ".cms/lang/uk_UA.UTF-8/template.mod.php",
    ),
);

// Выход если модуль отключен.
if ( ! empty( $cms["config"]["template.mod.php"]["disabled"] ) ) {
    return;
}

// Если шаблон еще не выбирался, то mini по умолчанию.
if ( empty( $cms["config"]["template.mod.php"]["template"] ) ) {
    $cms["config"]["template.mod.php"]["template"] = "mini";
}

// Эта переменная содержит активный шаблон.
// Дело в том, что админка это тоже шаблон admin.cms и когда происходит
// попадание на url админки, она подставляет свой шаблон независимо от того
// что указано в настройках.
$cms["template"] = $cms["config"]["template.mod.php"]["template"];
// Кроме того можно и в момент посещения сайта менять шаблоны целиком.
// Например для просмотра страницы в разных шаблонах или если понадобится
// делать личные кабинеты пользователей сайта и хочется целиком переключать шаблон.

// По умолчанию запись на диск сформированных страниц отключена.
if ( ! isset( $cms["config"]["template.mod.php"]["disable_write_to_disk"] ) ) {
    $cms["config"]["template.mod.php"]["disable_write_to_disk"] = true;
}

// Если заголовки для страницы еще не выставлялись, то выставляем отключение кэша.
if ( ! isset( $cms["config"]["template.mod.php"]["headers"] ) ) {
    $cms["config"]["template.mod.php"]["headers"] = "cache-control: max-age=0";
}

if ( is_admin() ) {
    hook_add_fn( "admin", "cms_template_admin" );
    hook_add_fn( "admin_header",  "cms_template_admin_header" );
    hook_add_fn( "api", "cms_template_api" );
}

// Вызов функции cms_template_template сделаем пораньше,
// чтобы в других модулях не нужно было ставить 20.
hook_add_fn( "template", "cms_template_template", 5 );
hook_add_fn( "echo", "cms_template_echo" );
hook_add_fn( "write", "cms_template_write" );

function cms_template_admin_header() {
    global $cms;
    if ( is_admin_header_on( "template.mod.php" ) ) {
        echo "        <link rel=stylesheet href='{$cms["base_path"]}css/template.css'>\n";
        echo "        <script src='{$cms["base_path"]}js/template.js'></script>\n";
    }
}

function cms_template_admin() {
    global $cms;

    // Если скрыто, то не выполнять лишнюю работу.
    if ( is_admin_header_on( "template.mod.php" ) ) {

    // Save settings
    if ( ! empty( $_POST["save_template"] ) ) {
        $cms["config"]["template.mod.php"]["template"] = $_POST["template"];
        $cms["config"]["template.mod.php"]["disable_write_to_disk"] = empty( $_POST["enable_cache"] );
        cms_save_config();
        if ( function_exists( "cms_clear_cache" ) ) {
            cms_clear_cache();
        }
        $cms["status"] = "302";
        $cms["status_302_location"] = $cms["base_path"] . $cms["config"]["admin.mod.php"]["admin_url"];
        return;
    }
    
    // Save template headers
    if ( ! empty( $_POST["save_template_headers"] ) ) {
        $cms["config"]["template.mod.php"]["headers"] = $_POST["template_headers"];
        cms_save_config();
        $cms["status"] = "302";
        $cms["status_302_location"] = $cms["base_path"] . $cms["config"]["admin.mod.php"]["admin_url"];
        return;
    }

    // Create menu item
    if ( empty( $cms["config"]["template.mod.php"]["menu"]["template"] ) ) {
        $cms["config"]["template.mod.php"]["menu"]["template"] = array(
            "title"    => "template_module_name",
            "sort"     => 70,
            "section"  => "settings",
        );
        cms_save_config();
    }

    $options = "";
    foreach( glob( "{$cms['cms_dir']}/*/html.php" ) as $path_html_php ) {
        $template = preg_replace( "/.*\/([^\/]+)\/html\.php/u", "$1", $path_html_php );
        if ( substr( $template, 0, 6 ) !== "admin." ) {
            $options .= "<div class=option value='{$template}'>{$template}</div>";
        }
    }

    $template_files_title = "<div class=title>" . __( "editable_files" ) . "</div>";
    $template_files = "";

    $tpl_dir = $cms["cms_dir"] . "/" . $cms["config"]["template.mod.php"]["template"];
    $head_len = strlen( $cms["site_dir"] ) + 1;
    foreach ( glob( "{$tpl_dir}/*" ) as $full_path ) {
        $ext = strtolower( pathinfo( $full_path, PATHINFO_EXTENSION ) );
        if ( in_array( $ext, array( "css", "php", "html", "less", "js" ) ) ) {
            $file = substr( $full_path, $head_len );
            $template_files .= "<div class=file>{$file}</div>";
        }
    }
    if ( empty( $template_files ) ) {
        $template_files = "<div class=info>" . __( "no_editable_files" ) . "</div>";
    }
    
    $tr_title   = __( "help" );
    $tr_current = __( "current_template" );
    $tr_save    = __( "save" );
    $tr_upl     = __( "install_template" );
    if ( $cms["config"]["template.mod.php"]["disable_write_to_disk"] ) {
        $enable_cache = "";
    } else {
        $enable_cache = "checked";
    }
    $headers_title = __( "headers_title" );

    // Справка для шаблона определяется в файле template.settings.php
    $help = __( "help", $cms["config"]["template.mod.php"]["template"] );
    if ( $help === "help" ) {
        $help = __( "no_template_help" );
    }

    $e_headers = htmlspecialchars( $cms['config']['template.mod.php']['headers'] );

    // Display settings
    $page = "
<div class=settings>

    <div class='panel install'>
        <div class='button upload'>
            <span>
                <input type=file name='myfile[]' multiple>
                <span>{$tr_upl}</span>
            </span>
        </div>
    </div>

    <div class='panel template'>
        <form method=post>
            <div class=title>{$tr_current}</div>
            <input name=template autocomplete=off type=hidden value='{$cms["config"]["template.mod.php"]["template"]}'>

            <div class='templates select widget'>
                <div class=selected>
                    <div class=value>{$cms["config"]["template.mod.php"]["template"]}</div>
                    <div class='icon down button'>
                        <span></span>
                    </div>
                </div>
                <div class=options>
                    {$options}
                </div>
            </div>

            <label>
                <input type=checkbox name=enable_cache {$enable_cache}>
                <span class=enable-cache>" . __( "enable_cache" ) . "</span>
            </label>
            
            <button class=button name=save_template value=save>
                <span>{$tr_save}</span>
            </button>
        </form>
    </div>

    <div class='panel files'>
        {$template_files_title}
        {$template_files}
    </div>

    <div class='panel manual'>
        <div class=title>{$tr_title}</div>
        {$help}
    </div>

    <form method=post class='panel headers'>
        <div class=title>{$headers_title}</div>
        <textarea name=template_headers rows=7>{$e_headers}</textarea>
        <button class=button name=save_template_headers value=save>
            <span>{$tr_save}</span>
        </button>
    </form>
    
</div>

<div class='editor template hidden'>
    <div class='window'>
        <div class=header>
            <div class='button close icon'>
                <span></span>
            </div>
            <div class='button minimize icon'>
                <span><span class=dash></span></span>
            </div>
            <div class='button save icon'>
                <span></span>
            </div>
            <span class=title></span>
        </div>
        <div class='textarea'>
            <textarea name=template></textarea>
        </div>
        <div class='snippets'>
            <div class=buttons>
                <div class=button title='Ctrl + 2' data-type=snip data-otag='@media (min-width: px) and (max-width: px) {\n    ' data-ctag='\n}' data-line=1 data-ch=4>
                    <span>@media (min) and (max)</span>
                </div>
                <div class=button title='Ctrl + /' data-type=snip data-otag='/* ' data-ctag=' */' data-len=3>
                    <span>/* comment */</span>
                </div>
                <a href='{$cms['base_path']}man/{$cms['config']['locale']}/hint_css.html' target=_blank>
                    " . __( "hint_css" ) . "
                </a>
                <a href='{$cms['base_path']}man/{$cms['config']['locale']}/hint_php.html' target=_blank>
                    " . __( "hint_php" ) . "
                </a>
                <a href='{$cms['base_path']}man/{$cms['config']['locale']}/hint_html.html' target=_blank>
                    " . __( "hint_html" ) . "
                </a>
            </div>
        </div>
    </div>
</div>
";

    $cms["admin_pages"]["template"] = $page;

    }

}


function cms_template_api() {
    global $cms;
    
    if ( ! empty( $_POST["fn"] ) ) {
        
        switch ( $_POST["fn"] ) {

            case "clear_cache":
                if ( function_exists( "cms_clear_cache" ) ) {
                    cms_clear_cache();
                }
                echo( json_encode( array(
                    "info_text"  => __( "cache_cleared" ),
                    "info_class" => "success",
                    "info_time"  => 5000,
                ) ) );
                return;
            break;

            case "get_template_file":
                $file_path = $cms["site_dir"] . "/" . $_POST["file"];
                $content = file_get_contents( $file_path );
                if ( $content !== false ) {
                    $hash = hash( "sha256", $content );
                    echo( json_encode( array( "result" => array(
                        "file" => $content,
                        "hash" => $hash,
                    ) ) ) );
                } else {
                    echo( json_encode( array( "error" => array(
                        "notify_html"  => __( "file_error_exists" ),
                    ) ) ) );
                }
                return;
            break;

            case "save_template_file":
                $file_path = $cms["site_dir"] . "/" . $_POST["file"];
                $content = file_get_contents( $file_path );
                if ( $content === false ) {
                    echo( json_encode( array( "error" => array(
                        "notify_html"  => __( "save_error_exists" ),
                    ) ) ) );
                    return;
                }
                // Сверяем контрольную сумму
                $hash = hash( "sha256", $content );
                if ( $hash !== $_POST["hash"] ) {
                    echo( json_encode( array( "error" => array(
                        "notify_html"  => __( "save_error_hash" ),
                    ) ) ) );
                    return;
                }
                // Сохраняем файл
                $file = file_put_contents( $file_path, $_POST["content"] );
                if ( $file === false ) {
                    echo( json_encode( array( "error" => array(
                        "notify_html"  => __( "save_error" ),
                    ) ) ) );
                    return;
                }
                // Очистка кэша
                if ( function_exists( "cms_clear_cache" ) ) {
                    cms_clear_cache();
                }
                echo( json_encode( array( "result" => array(
                    "notify_html"  => __( "saved" ),
                    "hash" => hash( "sha256", $_POST["content"] ),
                ) ) ) );
                return;
            break;

            case "install_template":
                $success = true;
                //$text = "dev.coffee-cms.ru";
                //if ( $cms["url"]["host"] !== $text )
                foreach ( $_FILES["myfile"]["name"] as $n => $name ) {
                    if ( $_FILES["myfile"]["error"][$n] ) {
                        $success = false;
                        $text = str_replace( "xxx", $name, __( "upload_error_xxx" ) );
                        break;
                    } else {
                        // Unpack Template
                        // Object Oriented Style for future compability with PHP 8
                        $zip = new ZipArchive;
                        if ( $zip->open( $_FILES["myfile"]["tmp_name"][$n] ) === true ) {
                            $zip->extractTo( $cms["site_dir"] );
                            $zip->close();
                            $text = __( "install_success" );
                        } else {
                            $success = false;
                            $text = str_replace( "xxx", $name, __( "cant_unzip_xxx" ) );
                            break;
                        }
                    }
                }

                if ( $success ) {
                    echo( json_encode( array(
                        "info_text"  => $text,
                        "info_class" => "success",
                        "info_time"  => 5000,
                    ) ) );
                    return;
                } else {
                    echo( json_encode( array(
                        "info_text"  => $text,
                        "info_class" => "error",
                        "info_time"  => 10000,
                    ) ) );
                    return;
                }
            break;

            // Принимает массив стилей
            case "template_save_css":
                foreach ( $_POST["styles"] as $name => $content ) {
                    file_put_contents( "{$cms["cms_dir"]}/{$_POST["tpl"]}/{$name}.css", $content );
                }
                // Очистка кэша
                if ( function_exists( "cms_clear_cache" ) ) {
                    cms_clear_cache();
                }
                echo( json_encode( array( "result" => true ) ) );
            break;
            // Только админ может отправить стили - дополнительные проверки не нужны.
            // При любом изменении .less файла, админ должен сам посмотреть страничку
            // при этом создадутся .css файлы и все остальные страницы будут использовать их.
            // Кеш обновляется для того чтобы если стили встраиваются в страницу,
            // то не было расхождений.

        }
    }
}


function cms_template_template() {
    global $cms;
    if ( is_file( $cms["url"]["file"] ) ) {
        // Запрашиваемый файл начинается с точки
        $len = strlen( $cms["cms_dir"] );
        $tail = substr( $cms["url"]["file"], $len );
        $contain_dot = strpos( $tail, "/." ) !== false;
        // Запрашивается php-файл
        $php_file = substr( $cms["url"]["file"], -4 ) === ".php";
        if ( $php_file || $contain_dot ) {
            $cms["status"]         = "404";
            $cms["output"]         = ""; // Предотвратить варнинги
        } else {
            $cms["output"]["from"] = $cms["url"]["file"];
            $cms["output"]["to"]   = $cms["site_dir"] . "/" . $cms["url"]["path"];
            $cms["status"]         = "200";
        }
    } else {
        // Проверяем что все .less файлы скомпилированы
        if ( is_admin() ) {
            $cms_dir_length = strlen( $cms["cms_dir"] ) + 1; // со слешем
            $less_arr = array();
            $mixin_arr = array();
            if ( $cms["template"] === "admin.cms" ) {
                $tpl = "css";
            } else {
                $tpl = $cms["template"];
            }
            $less_dir = "{$cms["cms_dir"]}/{$tpl}";
            $less_dir_length = strlen( $less_dir ) + 1; // со слешем
            $less_files = glob( "{$less_dir}/*.less" );
            $max_mixin_mtime = 0; // Обновить все если обновился миксин
            foreach ( $less_files as $less_file ) {
                if ( substr( $less_file, -11 ) === ".mixin.less" ) {
                    $max_mixin_mtime = max( $max_mixin_mtime, filemtime( $less_file ) );
                    $rel_file_path = str_replace( $cms["cms_dir"] . "/", "" , $less_file );
                    array_push( $mixin_arr, "<link rel=stylesheet/less href='{$cms["base_path"]}{$rel_file_path}'>" );
                } else {
                    $mtime = filemtime( $less_file );
                    // возможно тут можно переделать по образцу выше чтобы упростить логику
                    $name = substr( $less_file, $less_dir_length, strlen( $less_file ) - $less_dir_length - 5 );
                    $css_file = "{$cms["cms_dir"]}/{$tpl}/{$name}.css";

                    if ( is_file( $css_file ) ) {
                        $css_mtime = filemtime( $css_file );
                        if ( is_writeable( $css_file ) ) {
                            $needs_update = $css_mtime < $mtime || $css_mtime < $max_mixin_mtime;
                        } else {
                            $needs_update = false;
                        }
                    } else {
                        // css-файл отсутствует и директория доступна на запись
                        // чтобы не получить бесконечный цикл создания файла
                        $needs_update = is_writeable( dirname( $css_file ) ) ;
                    }

                    if ( $needs_update ) {
                        array_push( $less_arr, "{$cms["base_path"]}{$tpl}/{$name}.less" );
                    }
                }
            }
            if ( count( $less_arr ) > 0 ) {
                $html = array();
                // Файлы требующие компиляции
                foreach ( $less_arr as $link ) {
                    array_push( $html, "<link rel=stylesheet/less href='{$link}'>" );
                }
                $cms["output"]  = "<html style='background-color: #000;'><head>";
                $cms["output"] .= "<script src='{$cms["base_path"]}js/less.js'></script>";
                $cms["output"] .= "
<script>
    const url_api = '{$cms["base_path"]}{$cms["config"]["admin.mod.php"]["api_url"]}';
    const tpl = '{$tpl}';
</script>";
                $cms["output"] .= "<script src='{$cms["base_path"]}js/template_less.js'></script>";
                $cms["output"] .= implode( "", $mixin_arr ) . implode( "", $html );
                $cms["output"] .= "</head><body></body></html>";
                return;
            }
        }
        
        // Страница не изменилась
        if ( $cms["status"] === "304" ) {
            $cms["output"] = "";
            return;
        }

        // Шаблонизация
        ob_start();
        include( "{$cms["cms_dir"]}/{$cms["template"]}/html.php" );
        $cms["output"] = ob_get_clean();
    }
}


function send_cache_validation_headers( $page, $headers = array() ) {
    if ( ! $page || ! isset( $page["created"] ) || ! isset( $page["modified"] ) ) {
        return;
    }
    
    $modified = max( strtotime( $page["created"] ), (int) $page["modified"] );
    if ( ! $modified ) return;
    
    $last_modified_gmt = gmdate( 'D, d M Y H:i:s \G\M\T', $modified );
    header( 'Last-Modified: ' . $last_modified_gmt );
    
    foreach ( $headers as $header ) {
        header( $header );
    }
}


function cms_template_echo() {
    global $cms;

    if ( ! isset( $cms["output"] ) ) return;

    $headers = array();
    if ( substr( $cms["template"], 0, 6 ) !== "admin." ) {
        $lines = explode( "\n", $cms["config"]["template.mod.php"]["headers"] );
        foreach ( $lines as $line ) {
            $line = trim( $line );
            if ( ! empty( $line ) ) {
                $headers[] = $line;
            }
        }
    }

    if ( isset( $cms["output"] ) && is_array( $cms["output"] ) ) {
        if ( $cms["status"] === "200" ) {
            $types = array(
                "css" => "text/css",
                "js" => "application/javascript",
                "svg" => "image/svg+xml",
            );
            $ext = preg_replace( "/.*\./u", "", $cms["output"]["to"] );
            if ( isset( $types[$ext] ) ) {
                $mime = $types[$ext];
            } else {
                $mime = "";
            }
            header( "Content-Type: {$mime}" );
            header( "{$_SERVER['SERVER_PROTOCOL']} 200 OK" ); // fix server 404
            foreach ( $headers as $header ) {
                header( $header );
            }
            cms_readfile( $cms["output"]["from"], false );
        } else {
            header( "{$_SERVER['SERVER_PROTOCOL']} 404 Not Found" );
        }
    } else {
        switch ( $cms["status"] ) {
            case "200":
                header( "{$_SERVER['SERVER_PROTOCOL']} 200 OK" ); // fix server 404
                if ( isset( $cms["page"] ) ) {
                    send_cache_validation_headers( $cms["page"], $headers );
                }
                if ( isset( $cms["output"] ) ) {
                    echo $cms["output"];
                }
            break;
            case "304":
                header( "HTTP/1.1 304 Not Modified" );
                if ( isset( $cms["page"] ) ) {
                    send_cache_validation_headers( $cms["page"], $headers );
                }
            break;
            case "404":
                header( "{$_SERVER['SERVER_PROTOCOL']} 404 Not Found" );
                if ( isset( $cms["output"] ) ) {
                    echo $cms["output"];
                }
                // Для админа выводится текст страницы
                // а для анонима сообщение что не найдена
            break;
            case "301":
                header( "Location: {$cms["status_301_location"]}", true, 301 );
            break;
            case "302":
                header( "Location: {$cms["status_302_location"]}", true, 302 );
            break;
        }
    }
}


function cms_template_write() {
    global $cms;

    if ( ! empty( $cms["config"]["template.mod.php"]["disable_write_to_disk"] ) ) {
        return;
    }

    if ( $cms["status"] !== "200" ) {
        return;
    }
    
    // write to disk
    umask( 0 );

    if ( is_array( $cms["output"] ) ) {
        
        $dirs = explode( "/", str_replace( $cms["site_dir"] . "/", "", $cms["output"]["to"] ) );
        $dir  = $cms["site_dir"];
        $file_name = array_pop( $dirs );
        foreach ( $dirs as $path ) {
            $dir .= "/" . $path;
            if ( ! is_dir( $dir ) ) {
                mkdir( $dir, 0777, false );
            } elseif( is_file( $dir ) ) {
                unlink( $dir );
                mkdir( $dir, 0777, false );
            }
        }

        if ( ! file_exists( $cms["output"]["to"] ) ) {
            $tmp_name = $cms["site_dir"] . "/uploads/.tmp_" . cms_uid() . "_" . pathinfo( $cms["output"]["to"], PATHINFO_BASENAME );
            copy( $cms["output"]["from"], $tmp_name );
            rename( $tmp_name, $cms["output"]["to"] );
        }

    } else {

        // create dirs
        $dirs = explode( "/", $cms["url"]["path"] );
        $dir  = $cms["site_dir"];
        $file_name = array_pop( $dirs );
        foreach ( $dirs as $path ) {
            $dir .= "/" . $path;
            if ( ! file_exists( $dir ) ) {
                mkdir( $dir, 0777, false );
            } elseif( is_file( $dir ) ) {
                unlink( $dir );
                mkdir( $dir, 0777, false );
            }
        }

        if ( ! $file_name ) {
            $file = $dir . "/index.html";
        } else {
            $file = $dir . "/" . $file_name;
        }

        if ( ! file_exists( $file ) ) {
            $tmp_name = $cms["site_dir"] . "/uploads/.tmp_" . cms_uid() . "_" . pathinfo( $file, PATHINFO_BASENAME );
            file_put_contents( $tmp_name, $cms["output"], LOCK_EX );
            chmod( $tmp_name, 0666 );
            rename( $tmp_name, $file );
        }

    }
}


function cms_clear_cache() {
    global $cms;

    do_hook( "clear_cache" );

    // array for delete dirs
    $dirs = array();

    // search all pages in database
    if ( ! empty( $cms["pdo"] ) ) {
        try {
            $stmt = $cms["pdo"]->query( "SELECT url FROM pages" );
            while ( $page = $stmt->fetch( PDO::FETCH_ASSOC ) ) {
                $file = $cms["site_dir"] . "/" . $page["url"];

                // Если url заканчивается на / то будем пробовать удалять url/index.html
                if ( substr( $file, -1 ) === "/" ) {
                    $file .= "index.html";
                } elseif ( is_dir( $file ) ) {
                    // если не зканчивается на / но это директория, то тоже самое
                    $file .= "/index.html";
                }

                // Удаление файла
                if ( is_file( $file ) ) {
                    unlink( $file );
                }
                // FIXME: тут возможно удаление .cms/...

                // Добавление папок файла в очередь на удаление
                $dir = "/" . $page["url"];
                // Пока не корень сайта
                while ( $dir && $dir !== "/" && $dir !== "\\" ) {
                    $dir_to_rm = $cms["site_dir"] . $dir;
                    if ( ! in_array( $dir_to_rm, $dirs ) ) {
                        $dirs[] = $dir_to_rm;
                    }
                    $dir = dirname( $dir );
                }
            }
        } catch ( PDOException $e ) {
            error_log( "Cache clearing failed: " . $e->getMessage() );
        }
    }

    // delete template files
    $queue[] = $cms["cms_dir"];
    while ( $cur = array_shift( $queue ) ) {
        if ( is_dir( $cur ) ) {
            // add current dir to array for delete
            array_push( $dirs, str_replace( ".cms/", "", $cur ) );
            // search children files and dirs
            $queue = array_merge( $queue, glob( $cur . "/*" ) );
        } else {
            $file = str_replace( ".cms/", "", $cur );
            if ( is_file( $file ) ) unlink( $file );
        }
    }

    // delete dirs
    rsort( $dirs );
    foreach ( $dirs as $dir ) {
        if ( is_dir( $dir ) && $dir !== $cms["cms_dir"] && $dir !== $cms["site_dir"] ) {
            if ( is_dir_and_empty( $dir ) ) {
                rmdir( $dir );
            }
        }
    }

}
