<?php
require_once __DIR__ . '/CocoDB_alias.php';
class CocoDB_old
{
    public static $tableCache = [];
    public static $uploadColumns = [];
    public static $queryCaches = [];
    public static $getCaches = [];
    public static $storeDebugData = false;
    public static $debugData = [];
    public static $trackData = [];
    public static $redis = null;
    public static $redis_expireTime = 0;
    public static $force_load_cache = false;
    public static $force_redis = false;
    public static $force_redis_module = false;
    public static $force_redis_html = false;
    public static $force_json_cache_uploads = false;
    public static $pluginsConfig = [];
    public static $backTracePoint = null;
    public static $defaultRelationsDepth = 2;
    public static $noCacheURIS = []; // Array de expresiones regulares que contengan las URLS
    public static $noCacheTABLES = []; // Array de Tablas
    public static $allowedTranslateFields = null; // Campos permitidos para la traducción
    /**
     * Inserta registros en una tabla
     * @destacar
     * @category DB
     * @param table: Tabla de inserción
     * @param records: Lista de registros a insertar
     * @param functions: Array asociativo con funciones a aplicar a cada key
     * @param options: Lista de opciones posibles que pasarle al método
     * @return Número de registros insertados
     */
    static function insertRecords($table, $records, $functions = [], $options = [])
    {

        if (!isset($records[0])) {
            $records = [$records];
        }
        if (@$options["preSaveTempId"]) {
            $preSave = true;
        }

        list($ignoreFields, $ignoreSchema, $prefix) = self::parse_options($options);

        if (!$ignoreSchema) {
            $schema = @loadSchema($table);
            if (!@$schema) self::error(["error" => 'Error. Tabla no encontrada']);
        }


        $result = 0;
        $lastSaved = 0;

        foreach ($records as $record) :
            $sqlBase = self::prepareBaseSQL($prefix, $table, @$schema, false, $ignoreFields, $record);
            $record = self::unsetKeys($record, $ignoreFields);
            if (!in_array("num", @$ignoreFields) && isset($record["num"])) {
                $sqlBase = str_replace("num=NULL", "num=" . intval(@$record["num"]), $sqlBase);
                $record = self::unsetKeys($record, ["num"]);
            }
            self::insertOrUpdate($record, $sqlBase, $result, null, $prefix . $table, $functions, $ignoreSchema, @$schema, $options);
            $lastSaved = mysql_insert_id();
            foreach (self::$uploadColumns as $keyColumn => $uploadColumn) {
                foreach ($uploadColumn as $keyCol => $urlPath) {
                    if (!@$urlPath) continue;
                    self::insertRecords("uploads", [
                        "urlPath" => $urlPath,
                        "filePath" => realpath(__DIR__ . "/../../../" . $urlPath),
                        "fieldName" => $keyColumn,
                        "recordNum" => $lastSaved,
                        "tableName" => $table,
                        "createdTime" => date("Y-m-d H:i:s"),
                        "order" => time() + $keyCol,
                        "width" => 640,
                        "height" => 480
                    ], [], ["ignoreSchema" => true]);
                }
            }

        endforeach;


        if (@$preSave) {
            $query = "UPDATE {$prefix}uploads "
                . "   SET recordNum = LAST_INSERT_ID(), preSaveTempId = '' "
                . " WHERE tableName     = '" . mysql_real_escape_string($table) . "' AND "
                . "       preSaveTempId = '" . mysql_real_escape_string($options["preSaveTempId"]) . "'";

            mysql_query($query) or self::error(["error" => "MySQL Error: " . htmlspecialchars(mysql_error()) . "\n"]);

            $query = "UPDATE {$prefix}traducciones "
                . "   SET recordNum = LAST_INSERT_ID(), preSaveTempId = '' "
                . " WHERE tableName     = '" . mysql_real_escape_string($table) . "' AND "
                . "       preSaveTempId = '" . mysql_real_escape_string($options["preSaveTempId"]) . "'";
            mysql_query($query) or self::error(["error" => "MySQL Error: " . htmlspecialchars(mysql_error()) . "\n"]);
        }

        if (@$options['generate_category_metadata']) {
            self::updateCategoryMetadata($table);
        }

        if (@$options['return_last_id']) {
            return $lastSaved;
        }

        return $result;
    }

    /**
     * Elimina registros de una tabla
     * @destacar
     * @category DB
     * @param table: Tabla de la que vamos a eliminar registros
     * @param where: Array asociativo que recoge campo => valor, operador, or
     * @return boolean que indica si se pudo ejecutar la consulta
     */
    static function deleteRecords($table, $where, $options = [])
    {
        list($_, $_, $prefix) = self::parse_options($options);

        $where = self::parse_where($where, $table, $prefix);

        if (!@$where) return false;

        $sql = "DELETE FROM $prefix$table WHERE $where";

        if (@$options['dieBeforeQuery'] && $table == "uploads") {
            self::error(["info" => $sql]);
        }

        $q = mysql_query($sql);
        if (!$q) return false;
        return true;
    }

    /**
     * Actualiza registros en una tabla
     * @destacar
     * @category DB
     * @param table: Tabla de inserción
     * @param records: Lista de registros a insertar
     * @param functions: Array asociativo con funciones a aplicar a cada key
     * @param options: Lista de opciones posibles que pasarle al método
     * @return Número de registros insertados
     */
    static function updateRecords($table, $records, $where, $functions = [], $options = [])
    {
        global $TABLE_PREFIX;

        if (!isset($records[0])) {
            $records = [$records];
        }

        list($ignoreFields, $ignoreSchema, $prefix) = self::parse_options($options);

        if (!$ignoreSchema) {
            $schema = @loadSchema($table);
            if (!@$schema) self::error(["error" => 'Error. Tabla no encontrada']);
        }

        $result = 0;
        foreach ($records as $record) :
            $sqlBase = self::prepareBaseSQL($prefix, $table, @$schema, true, $ignoreFields, $record);
            $lastNum = @$record['num'];
            $record = self::unsetKeys($record, $ignoreFields);
            // Está comentado, pero no se si hace falta, si se descomenta se rompe el guardar del Builder (Plugin)
            // if (@$schema['menuType'] == 'category' && !isset($record['parentNum'])) {
            //     continue;
            // }

            self::insertOrUpdate($record, $sqlBase, $result, $where, $prefix . $table, $functions, $ignoreSchema, @$schema, $options);

            if (@$lastNum && @$options['delete_upload_nums']) {
                // El Prefix se añade "por la cara" porque el show_columns peta sin el
                self::deleteRecords("uploads", [
                    ["column" => "num", "value" => $options['delete_upload_nums'], "operator" => "IN"]
                ], ["prefix" => $TABLE_PREFIX]);
            }

            foreach (self::$uploadColumns as $keyColumn => $uploadColumn) {
                // El Prefix se añade "por la cara" por lo mismo que el delete_upload_nums aunque este no está probado
                if (@$lastNum && @$options['delete_old_uploads']) {
                    self::deleteRecords("uploads", [
                        "fieldName" => $keyColumn,
                        "recordNum" => $lastNum,
                        "tableName" => $table,
                    ], ["prefix" => $TABLE_PREFIX]);
                }

                foreach ($uploadColumn as $keyCol => $urlPath) {

                    if (@$lastNum && @$options['insert_new_uploads']) {
                        self::insertRecords("uploads", [
                            "urlPath" => $urlPath,
                            "filePath" => realpath(__DIR__ . "/../../../" . $urlPath),
                            "fieldName" => $keyColumn,
                            "recordNum" => $lastNum,
                            "tableName" => $table,
                            "createdTime" => date("Y-m-d H:i:s"),
                            "order" => time() + $keyCol,
                            "width" => 640,
                            "height" => 480
                        ], [], ["ignoreSchema" => true]);
                    }
                }
            }
        endforeach;

        if (@$options['generate_category_metadata']) {
            self::updateCategoryMetadata($table);
        }

        return $result;
    }


    /**
     * Hace el insert o update de un único registro
     * @param record: Registro con el que vamos a operar
     * @param sqlBase: SQL Base
     * @param result: Número de registros con los que hemos operado. In-out
     * @param where: Where de la operación (solo si es un UPDATE)
     * @param table: Tabla de la operación
     * @param functions: Lista de funciones con las que podemos parsear un determinado valor
     * @param ignoreSchema: Boolean que indica si vamos a ignorar el Schema o no
     * @param schema: Schema de la tabla
     */
    public static function insertOrUpdate($record, $sqlBase, &$result, $where = null, $table = null, $functions = null, $ignoreSchema = false, $schema = null, $options = [])
    {
        $sql = $sqlBase;
        self::$uploadColumns = [];
        foreach ($record as $key => $value) :
            $column_exists = self::column_exists($key, @$schema, $table);
            if (!$column_exists) continue;
            if (is_array($column_exists) && $column_exists["type"] === "upload") {
                if (!is_array($value)) $value = [$value];
                if (!isset(self::$uploadColumns[$key])) self::$uploadColumns[$key] = [];
                foreach ($value as $val) {
                    self::$uploadColumns[$key][] = $val;
                }
                continue;
            }

            if (is_array($functions) && isset($functions[$key]) && is_callable($functions[$key])) {
                $value = $functions[$key]($value);
            } else if (!$ignoreSchema) {
                $value = self::parse_value_schema($value, $schema, $key);
            }

            if ($value === null) {
                continue;
                // return false;
            }

            if (is_array($value)) {
                $value = json_encode($value);
            }

            $sql .= ", `$key`='" . mysql_real_escape_string($value) . "'";
        endforeach;

        if (@$where) {
            $where = self::parse_where($where, $table);
            $sql .= " WHERE " . $where;
        }

        if (@$options['dieBeforeQuery'])
            self::error(["info" => $sql]);

        if (mysql_query($sql)) {
            $result++;
        } else {
            self::error(["error" => mysql_error(), "sql" => $sql]);
        }
    }

    public static function error($array = [])
    {
        if (class_exists('API') && class_exists('ApiError')) {
            API::$die = true;
            API::error(new ApiError(json_encode($array)));
        } else {
            die(json_encode($array));
        }
    }
    /**
     * Función que prepara el SQL base dependiendo del schema y de si es INSERT o UPDATE
     * @param prefix: Prefijo de la tabla
     * @param table: Tabla de la operación
     * @param schema: Schema de la tabla
     * @param update: Boolean que indica si vamos a actualizar o insertar
     * @return sql
     */
    public static function prepareBaseSQL($prefix, $table, $schema = null, $update = false, $ignoreFields = [], $record = [])
    {
        $operation = $update ? "UPDATE" : "INSERT INTO";
        if (@$schema) {
            $d = date('Y-m-d H:i:s');
            $t = time();

            $user = 1;
            if(class_exists('API')) {
                $user = @Api::$user["num"] ?: 1;
            }

            $keysBase = ["createdDate" => $d, "createdByUserNum" => $user];


            $keys = [
                "category" => ["globalOrder" => 0, "siblingOrder" => 0, "lineage" => '', "depth" => 0, "breadcrumb" => '', "parentNum" => 0],
                "multi" => ["dragSortOrder" => $t]
            ];

            $sqlBase = "$operation $prefix$table SET updatedDate='$d', updatedByUserNum='$user'";

            if (!$update) {
                $sqlBase .= ", num=NULL";

                foreach ($keysBase as $keyBase => $valueBase) {
                    if (isset($record[$keyBase])) continue;
                    if (is_string($valueBase)) {
                        $sqlBase .= ", `" . $keyBase . "`='" . $valueBase . "'";
                    } else {
                        $sqlBase .= ", `" . $keyBase . "`=" . ($valueBase ?: 'NULL');
                    }
                }

                if (@$keys[$schema['menuType']]) {
                    foreach ($keys[$schema['menuType']] as $key => $value) {
                        if (isset($record[$key])) continue;
                        if (is_string($value)) {
                            $sqlBase .= ", `" . $key . "`='" . $value . "'";
                        } else {
                            $sqlBase .= ", `" . $key . "`=" . $value;
                        }
                    }
                }
            }
        } else {
            $sqlBase = "$operation $prefix$table SET num=" . ($update ? "num" : "NULL");
        }
        return $sqlBase;
    }

    /**
     * Parsea las opciones de los métodos
     * @return lista con las opciones ignoreFields, ignoreSchema y prefix
     */
    public static function parse_options($options)
    {
        global $TABLE_PREFIX;

        $ignoreFields = @$options["forceNum"] ? [] : ['num'];

        if (@$options['ignoreFields']) {
            $ignoreFields = array_merge($ignoreFields, $options['ignoreField']);
        }

        $ignoreSchema = @$options['ignoreSchema'] ?: false;
        $prefix = isset($options["prefix"]) ? $options["prefix"] : $TABLE_PREFIX;
        return [$ignoreFields, $ignoreSchema, $prefix];
    }

    /**
     * Comprueba si una columna existe en una tabla
     * @return boolean que indica si existe o no la columna
     */
    public static function column_exists($key, $schema, $table, $prefix = "")
    {
        global $TABLE_PREFIX;

        if (isset(self::$tableCache[$table]) && isset(self::$tableCache[$table][$key])) {
            return self::$tableCache[$table][$key];
        }
        if ($schema && isset($schema[$key])) {
            return $schema[$key];
        }

        $result = mysql_query("SHOW COLUMNS FROM `$prefix$table` LIKE '$key'");

        $exists = mysql_num_rows($result) > 0;
        self::cache_column($table, $key, $exists);

        return $exists;
    }

    /**
     * Cachea la comprobación de una columna en una tabla
     */
    public static function cache_column($table, $column, $exists)
    {
        if (!isset(self::$tableCache[$table])) {
            self::$tableCache[$table] = [];
        }
        if (!isset(self::$tableCache[$table][$column])) {
            self::$tableCache[$table][$column] = $exists;
        }
    }

    /**
     * Parsea el valor dependiendo del tipo de campo
     * @return valor parseado
     */
    public static function parse_value_schema($value, $schema, $key)
    {
        if (isset($schema[$key]) && isset($schema[$key]['type'])) {
            switch ($schema[$key]['type']) {
                case 'list':
                    switch ($schema[$key]['listType']) {
                        case 'pulldownMulti':
                            if (is_array($value)) {
                                $value = "\t" . join("\t", $value) . "\t";
                            }
                            break;
                        default:
                            break;
                    }
                    break;
                case 'multitext':
                    if (is_array($value)) {
                        $value = json_encode($value);
                    }
                    break;
                case 'checkbox':
                    $value = @$value ? 1 : 0;
                    break;
                default:
                    break;
            }
        }
        return $value;
    }

    /**
     * Parsea el where pasado por parámetro
     * - or: Si se envía a true usa OR como enlace en lugar de AND
     * - not: Si se envía a true se usa
     * - operador: LIKE, IN, != o =
     * - raw_key: evita que ponga la comillas en la key y que no compruebe si existe
     *
     * @param where: String o array
     * @return where
     */
    public static function parse_where($where, $table, $prefix = "")
    {
        $builtWhere = "";
        $add_parenthesis = false;
        if (is_array($where)) {

            foreach ($where as $key => $w) :
                if (is_array($w)) {
                    if (!isset($w["value"]) || !isset($w["column"])) return false;
                    if (!isset($w["operator"])) $w["operator"] = '=';
                    $key = $w["column"];

                    if ($table && !@$w["raw_key"] && !self::column_exists($key, null, $table, $prefix)) return false;

                    $value = $w["value"];
                    $enlace = @$w["or"] ? ") OR (" : "AND";
                    if (@$w["or"]) $add_parenthesis = true;
                    $not = @$w["not"] ? "NOT " : "";

                    switch (strtoupper(@$w["operator"])) {
                        case "LIKE":
                            // Cambiado para DAXAuto... aunque no recuerdo el motivo exacto.
                            // $value = "'".mysql_real_escape_string($value)."'";
                            $value = "'" . str_replace('\\\\t', '\\t', mysql_real_escape_string($value)) . "'";
                            $operador = "LIKE";
                            break;
                        case "IS NULL":
                            $value = "NULL";
                            if ($not == '') {
                                $operador = "IS";
                            } else {
                                $operador = "IS NOT";
                                $not = '';
                            }
                            break;
                        case "IN":
                            if (is_array($value)) {
                                $value = join(", ", array_map(function ($a) {
                                    if (is_int($a)) return intval($a);
                                    if (is_numeric($a)) return floatval($a);
                                    return "'" . mysql_real_escape_string($a) . "'";
                                }, $value));
                            }
                            $value = "(" . $value . ")";
                            $operador = "IN";
                            break;
                        case "!=":
                            $value = "'" . mysql_real_escape_string($value) . "'";
                            $operador = "!=";
                            break;
                        default:
                            $value = "'" . mysql_real_escape_string($value) . "'";
                            if (in_array(strtoupper($w["operator"]), ['<', '>', '<=', '>=', '='])) {
                                $operador = $w["operator"];
                            } else {
                                $operador = "=";
                            }
                    }

                    if (@$builtWhere) $builtWhere .= " $enlace ";
                    if(!@$w["raw_key"]) {
                        $builtWhere .= "$key $not$operador $value";
                    } else {
                        $builtWhere .= "`$key` $not$operador $value";
                    }
                } else {
                    if (@$builtWhere) $builtWhere .= " AND ";
                    $builtWhere .= "`$key`='" . mysql_real_escape_string($w) . "'";
                }
            endforeach;
        } else {
            $builtWhere = $where;
        }
        if ($add_parenthesis) return "(" . $builtWhere . ")";
        return $builtWhere;
    }

    /**
     * Elimina del primer array las claves pasadas en el segundo parámetro
     * @param array: Array del que vamos a eliminar las keys
     * @param keys: Array que contiene las keys que queremos eliminar
     * @return nuevo array
     */
    public static function unsetKeys($array, $keys)
    {
        $c = $array;
        foreach ($keys as $removeKey) {
            unset($c[$removeKey]);
        }
        return $c;
    }

    /**
     * Setea un punto de BackTrace
     * @param string: string informativo
     */
    static function setBacktracePoint($string)
    {
        self::$backTracePoint = $string;
    }
    /**
     * Devuelve la información de TrackData ( uso en acai code )
     * @return trackData
     */
    static function getTrackData()
    {
        return self::$trackData;
    }
    /**
     * Setea información de track para saber por donde va toda la web ( uso en acai code )
     * @return pushed data
     */
    static function setTrackData($init = false, $type = null, $id = null, $data = [])
    {
        if ($init) {
            

            $pushedData = [
                "ip"    => $_SERVER["REMOTE_ADDR"],
                "timestamp" => round(floatval(microtime(true) * 1000), 4),
                "totalTime" => 0,
                "host" => $_SERVER["HTTP_HOST"],
                "url" => $_SERVER["REQUEST_URI"],
                "trackData" => []
            ];
            self::$trackData[] = $pushedData;
        } else {
            //            if (count(self::$trackData[count(self::$trackData)-1]["trackData"])){
            //                $prevTimestamp = self::$trackData[count(self::$trackData)-1]["trackData"][count(self::$trackData[count(self::$trackData)-1]["trackData"])-1]["timestamp"];
            //            }else{
            //                
            //            }
            $prevTimestamp = self::$trackData[count(self::$trackData) - 1]["timestamp"];
            $pushedData = [
                "timestamp" => round((floatval(microtime(true) * 1000) - $prevTimestamp), 4),
                "type" => $type,
                "id" => $id,
                "transferKeys" => !isset($data[0]) ? array_keys($data) : (is_array($data[0]) ? array_keys($data[0]) : ["undefined" => $data[0]])
                //                "data" => $data
            ];
            self::$trackData[count(self::$trackData) - 1]["trackData"][] = $pushedData;
            self::$trackData[count(self::$trackData) - 1]["totalTime"] = $pushedData["timestamp"];
            $prevPercent = 0;
            foreach (self::$trackData[count(self::$trackData) - 1]["trackData"] as $cont => $trackData) {
                $percent = ($trackData["timestamp"] * 100) / self::$trackData[count(self::$trackData) - 1]["totalTime"];
                self::$trackData[count(self::$trackData) - 1]["trackData"][$cont]["percent"] = round($percent, 2);
                self::$trackData[count(self::$trackData) - 1]["trackData"][$cont]["initPercent"] = $prevPercent;
                self::$trackData[count(self::$trackData) - 1]["trackData"][$cont]["widthPercent"] = $percent - $prevPercent;
                $prevPercent = $percent;
            }

            /*self::$trackData[count(self::$trackData)-1]["totalTime"] += $pushedData["timestamp"];
            self::$trackData[count(self::$trackData)-1]["totalTime"] = round(self::$trackData[count(self::$trackData)-1]["totalTime"],4);
                        
            self::$trackData[count(self::$trackData)-1]["trackData"][] = $pushedData;
            
            $sum = 0;
            foreach( self::$trackData[count(self::$trackData)-1]["trackData"] as $cont => $trackData){
                $percent = ($trackData["timestamp"] * 100)/self::$trackData[count(self::$trackData)-1]["totalTime"];
                
                self::$trackData[count(self::$trackData)-1]["trackData"][$cont]["percent"] = $sum;
                $sum+=$percent;
                $sum = round($sum,2);
            }*/
        }
        return $pushedData;
    }

    /**
     * Muestra la variable debug
     * @return html content
     */
    static function showDebug($formated = false, $index = -1)
    {
        if (!$formated) return json_encode(self::$debugData, JSON_PRETTY_PRINT);
        if (!self::$debugData) return "";
        $result = '
            <link href="https://unpkg.com/tailwindcss@^1.0/dist/tailwind.min.css" rel="stylesheet">
            <div class="container mx-auto border p-4 flex flex-col pb-2">
        ';
        foreach (self::$debugData as $cont => $debugQuery) {
            if ($index > -1 && $cont != $index) continue;
            $result .= '<div class="w-full ">';
            $result .= '<div class="border p-2 bg-green-200 text-green-800 text-xs border-green-400 mb-2 break-word block overflow-auto whitespace-normal">';
            $result .= $debugQuery["hora"] . "<br>";
            if (self::$backTracePoint) $result .= self::$backTracePoint . "<br>";
            $result .= $debugQuery["query"] . "<br>" . $debugQuery["time"] . " microsegundos<br>";
            $result .= "<button onclick='this.parentNode.parentNode.querySelector(\".info\").classList.toggle(\"hidden\")' class='text-white py-1 px-2 mt-2 mr-1 bg-green-500 hover:bg-green-400 focus:outline-none inline-block'>Ver resultado</button>";
            $result .= "<button onclick='this.parentNode.parentNode.querySelector(\".back\").classList.toggle(\"hidden\")' class='text-white py-1 px-2 mt-2 bg-blue-500 hover:bg-blue-400 focus:outline-none inline-block'>Ver backtrace</button>";
            $result .= '</div>';
            $result .= '<pre class="info hidden bg-gray-200 p-4 text-gray-600 whitespace-normal text-xs">';
            $result .= str_replace("  ", "&nbsp;&nbsp;", nl2br(json_encode($debugQuery["records"], JSON_PRETTY_PRINT)));
            $result .= '</pre>';
            $result .= '<pre class="back hidden bg-blue-200 p-4 text-blue-800 whitespace-normal text-xs">';
            $result .= str_replace("  ", "&nbsp;&nbsp;", nl2br(json_encode(debug_backtrace(), JSON_PRETTY_PRINT)));
            $result .= '</pre>';
            $result .= '</div>';
        }
        $result .= '</div>';
        return $result;
    }
    /**
     * Parsea un record dado de un resultado de busqueda en la base de datos añadiendo información extra de valor
     */
    static function parseGetRecord(&$record, $firstTable, $schema, $options = [], $uploadsResult = [])
    {
        global $TABLE_PREFIX;
        $record["tableName"] = $firstTable;

        if (@$uploadsResult) {
            foreach ($schema as $fieldName => $fieldValue) {
                if (!is_array($fieldValue)) continue;
                if (@$fieldValue["type"] != "upload") continue;

                if (@$uploadsResult[$firstTable][$fieldName][$record['num']]) {
                    $record[$fieldName] = $uploadsResult[$firstTable][$fieldName][$record['num']];
                }
            }
        }

        foreach ($record as $recordKey => $recordValue) {
            $schemaField = @$schema[$recordKey];
            if (!@$schemaField || !is_array($schemaField)) continue;
            if (@$options["relations"] && is_array($options["relations"]) && !in_array($recordKey, $options["relations"])) continue;
            switch (@$schemaField["type"]) {
                case "list":
                    switch ($schemaField["optionsType"]) {
                        case "query":
                            $query = getEvalOutput($schemaField['optionsQuery']);
                            if (!isset(self::$queryCaches[md5($query)])) {
                                self::$queryCaches[md5($query)] = mysql_query_fetch_all_assoc($query);
                            }
                            preg_match('/FROM\s+(.*)\s+/', $query . " ", $matches);
                            $tableQuery = null;
                            if (@$matches[1]) $tableQuery = str_replace($TABLE_PREFIX, "", trim($matches[1]));

                            $result = array_filter(self::$queryCaches[md5($query)], function ($rec) use ($recordValue) {
                                return $recordValue == $rec[array_keys($rec)[0]];
                            });
                            $result = array_merge(array_map(function ($rec) use ($tableQuery) {
                                if ($tableQuery) $rec["tableName"] = $tableQuery;
                                return $rec;
                            }, $result));
                            $record[$recordKey . "_bd"] = $result;
                            break;
                        case "text":
                            $optionsText = array_filter(explode("\n", $schemaField["optionsText"]));
                            $optionsList = [];
                            foreach ($optionsText as $option) {
                                $sepOption = explode("|", $option);
                                if (!isset($sepOption[1])) $sepOption[1] = $sepOption[0];
                                $optionsList[$sepOption[0]] = $sepOption[1];
                            }
                            // Anael: evitamos que se haga un explode de los valores null (casos extraños como el blog).
                            $resultDatas = [];
                            if(@$recordValue) $resultDatas = explode("\t", $recordValue);
                            $record[$recordKey . "_bd"] = [];
                            foreach ($resultDatas as $resultData) {
                                if (isset($optionsList[$resultData])) {
                                    $record[$recordKey . "_bd"][] = ["key" => $resultData, "value" => t_var($optionsList[$resultData])];
                                } else {
                                    $record[$recordKey . "_bd"][] = ["key" => $resultData, "value" => t_var($resultData)];
                                }
                            }
                            break;
                        case "table":

                            $nums = array_filter(explode("\t", mysql_real_escape_string($recordValue ?? '')));
                            if (@$nums && @$options["relationsDepth"]) {
                                /*$hash_query_caches = 'query_all_'.$schemaField["optionsTablename"] . '_' . $options["relationsDepth"];
                                if(!isset(self::$queryCaches[$hash_query_caches])) {
                                    self::$queryCaches[$hash_query_caches] = self::get($schemaField["optionsTablename"], null, null, null, [
                                        "relationsDepth" => intval(@$options["relationsDepth"])-1,
                                        "debug" => @$options["debug"],
                                        "translates" => @$options["translates"],
                                        "uploads" => @$options["uploads"]
                                    ]);
                                }
                                $cache_filter_by_key = $schemaField["optionsValueField"];
                                $cache_filter_by_values = $nums;
                                $record[$recordKey."_bd"] = array_values(array_filter(self::$queryCaches[$hash_query_caches], function($each) use($cache_filter_by_key, $cache_filter_by_values) {
                                    return in_array($each[$cache_filter_by_key], $cache_filter_by_values);
                                }));*/
                                // SE HA ELIMINADO ESTE SCRIPT PORQUE DUPLICABA EL TIEMPO DE RESPUESTA EN BANANA
                                //$record[$recordKey."_bd"] = self::get($schemaField["optionsTablename"], $schemaField["optionsValueField"]." IN ('".join("','", $nums)."')", null, null, ["relationsDepth" => intval(@$options["relationsDepth"])-1,"debug" => @$options["debug"]]);
                                $record[$recordKey . "_bd"] = self::get($schemaField["optionsTablename"], $schemaField["optionsValueField"] . " IN ('" . join("','", $nums) . "')", null, null, [
                                    "relationsDepth" => intval(@$options["relationsDepth"]) - 1,
                                    "debug" => @$options["debug"],
                                    "translates" => @$options["translates"],
                                    "uploads" => @$options["uploads"]
                                ]);
                            }
                            break;
                        default:
                    }
                    break;
                case "multitext":
                    $record[$recordKey . "_bd"] = json_decode($recordValue ?? '', true);
                    break;
                default:
            }
        }
        // Translates
        if (@$options["translates"]) {
            $idiomaActual = @$_REQUEST["idioma"];
            $_REQUEST["idioma"] = @$options["translates"];
            if (@$_REQUEST["idioma"]) {
                $record = self::t_recursivo($record, null);
            }
            $_REQUEST["idioma"] = $idiomaActual;
        }

        $record["breadcrumbField"] = @$schema["breadcrumbField"];
        // Si es parentNum ponemos valores por defecto
        if (@$record["breadcrumbField"] == "parentNum") {
            $record["optionsTablename"] = $record["tableName"];
            $record["optionsValueField"] = "num";
        } else if (@$record["breadcrumbField"]) {
            // Si no es parentNum, ponemos los que dicte el schema
            $record["optionsTablename"] = @$schema[$schema["breadcrumbField"]]["optionsTablename"];
            $record["optionsValueField"] =  @$schema[$schema["breadcrumbField"]]["optionsValueField"];
        }

        // Para el campo principal (para la generación de enlaces y el breadcrumb)
        if (@$record["name"]) {
            $record["mainFieldBreadcrumb"] = $record["name"];
        } else if (@$record["title"]) {
            $record["mainFieldBreadcrumb"] = $record["title"];
        } else if (@$record["titulo"]) {
            $record["mainFieldBreadcrumb"] = $record["titulo"];
        } else if (@$record["nombre"]) {
            $record["mainFieldBreadcrumb"] = $record["nombre"];
        } else {
            foreach ($schema as $key => $value) :
                if (!is_array($value)) continue;
                if (@$value["type"] == "textfield" && $key != "enlace") {
                    $record["mainFieldBreadcrumb"] = $record[$key];
                    break;
                }
            endforeach;
        }
    }
    /**
     * Obtiene los uploads de una tabla y los cachea
     * @return un array con todos los uploads
     */
    static function getUploadsResults($options)
    {
        global $TABLE_PREFIX;
        $pathCacheUploads = __DIR__ . "/../../../cache/";
        if (!file_exists($pathCacheUploads)) {
            mkdir($pathCacheUploads);
        }
        $fileName = $pathCacheUploads . "uploads-" . date("Y-m-d", time()) . "-" . $_SERVER['HTTP_HOST'] . ".json";
        if (!file_exists($fileName)) {
            $fields_to_select = "num, `order`, tableName, fieldName, recordNum, filePath, urlPath, info1, info2, info3, info4, info5, alt";
            $uploads = mysql_query_fetch_all_assoc("SELECT $fields_to_select FROM " . $TABLE_PREFIX . "uploads ORDER BY `order` ASC");
            $uploadsResult = [];
            foreach ($uploads as $upload) {
                $table = $upload['tableName'];
                $field = $upload['fieldName'];
                $num = $upload['recordNum'];
                if (!isset($uploadsResult[$table])) $uploadsResult[$table] = [];
                if (!isset($uploadsResult[$table][$field])) $uploadsResult[$table][$field] = [];
                if (!isset($uploadsResult[$table][$field][$num])) $uploadsResult[$table][$field][$num] = [];
                $uploadsResult[$table][$field][$num][] = $upload;
            }
            file_put_contents($fileName, json_encode($uploadsResult));
        } else {
            if ($options['useAbsoluteUrls']) {
                $uploadsResult = json_decode(str_replace('"urlPath":"\/cms', '"urlPath":"https:\/\/' . $_SERVER['HTTP_HOST'] . '\/cms', file_get_contents($fileName)), true);
            } else {
                $uploadsResult = json_decode(file_get_contents($fileName), true);
            }
            // $uploadsResult = isset($uploadsResult[$tableName]) ? $uploadsResult[$tableName] : [];
        }
        return $uploadsResult;
    }
    /**
     * Obtiene los uploads consultándolos a base de datos registro por registro
     * @return void
     */
    static function getUploadsResultsFromRecord(&$record, $firstTable, $schema, $options)
    {
        global $TABLE_PREFIX;
        if (!is_array($schema)) return;
        $record["tableName"] = $firstTable;
        $fields_to_select = "num, `order`, tableName, fieldName, recordNum, filePath, urlPath, info1, info2, info3, info4, info5, alt";
        foreach ($schema as $schemaKey => $schemaField) {
            if (!is_array($schemaField)) continue;
            switch (@$schemaField["type"]) {
                case "upload":
                    $uploads = mysql_query_fetch_all_assoc("SELECT " . $fields_to_select . " FROM " . $TABLE_PREFIX . "uploads WHERE tableName = '" . $firstTable . "' and fieldName = '" . $schemaKey . "' and recordNum = " . $record["num"] . " ORDER BY `order` ASC");
                    if (@$options['useAbsoluteUrls'] && is_array($uploads)) {
                        $uploads = array_map(function ($each) {
                            if (strpos($each['urlPath'], '/cms') === 0) {
                                $each['urlPath'] = str_replace('/cms', 'https://' . $_SERVER['HTTP_HOST'] . '/cms', $each['urlPath']);
                            }
                            return $each;
                        }, $uploads);
                    }
                    $record[$schemaKey] = @$uploads;
                    break;
                default:
            }
        }
    }

    /**
     * Recupera todas las configuraciones de los plugins en una sola consulta y devuelve el resultado si existe el plugin buscado en un array general
     */
    
    static function getPluginsConfig($table,$where){
        if (!self::$pluginsConfig){
            $pluginsConfig = mysql_query_fetch_all_assoc("SELECT * FROM aux_plg_config ORDER BY num DESC");
            
            self::$pluginsConfig = [];
            foreach($pluginsConfig as $pluginConfig){
                if (!@self::$pluginsConfig[$pluginConfig["plugin"]]) self::$pluginsConfig[$pluginConfig["plugin"]] = $pluginConfig;
            }
        }
        
        $auxWhere = str_replace(" ","",strtolower(trim($where)));
        $auxWhere = str_replace("plugin=","",$auxWhere);
        $auxWhere = str_replace("'","",$auxWhere);
        $auxWhere = str_replace('"','',$auxWhere);

        return @self::$pluginsConfig[$auxWhere] ?: [];
    } 

    /**
     * Obtiene registros de una tabla
     * @param table: Tabla de la que vamos a eliminar registros
     * @param where: string con el where o array de condiciones ( condicion ["field" => num,"operator" => "=","value" => 1] )
     * @param order: string
     * @param limit: string con el limit o array de limite y offset ( ["limit" => 10,"offset" => 20] )
     * @param options: Array asociativo que recoge opciones
          @option : debug => Boolean
          @option : translates => string con el idioma
          @option : uploads => Boolean
          @option : groupBy => string
          @option : aggregates => array de aggregates
          @option : relations => Boolean o array de campos en los que emitir las relaciones ( ['islas'] )
          @option : relationsDepth => Int

     * @return todos los registros
     */
    static function get($table, $where, $order = null, $limit = null, $options = [])
    {
        global $TABLE_PREFIX;

        /*if ($table == "aux_plg_config"){
            // Evitar demasiadas consultas a aux_plg_config
            $resultPlugin = self::getPluginsConfig($table,$where);        
            return $resultPlugin;
            Desactivado temporalmente porque se pierden los márgenes
        }*/
        
        $microtime = microtime(true);
        $optionsDefault = [
            "debug" => false,
            "translates" => @$_REQUEST["idioma"],
            "uploads" => true,
            "useAbsoluteUrls" => false,
            "groupBy" => null,
            "ignoreSchema" => false,
            "aggregates" => [],
            "relations" => true,
            "redis" => null,
            "onlyFields" => null,
            "redis_expire" => 60,
            "relationsDepth" => self::$defaultRelationsDepth,
            "dieBeforeQuery" => false,
            "prefix" => $TABLE_PREFIX
        ];

        // ALIAS PARA JORDAN
        if (@$options["ignoreSchemas"]) $options["ignoreSchema"] = $options["ignoreSchemas"];

        foreach ($optionsDefault as $key => $value) {
            if (!isset($options[$key])) $options[$key] = $value;
        }

        
        if (self::$force_redis) {
            $options["redis"] = is_null($options["redis"]) ? true : $options["redis"];
            if (self::$redis_expireTime) $options["redis_expire"] = self::$redis_expireTime;
        }


        // EN CASO DE PEDIR CACHE REDIS INSTANCIAMOS Y CONECTAMOS
        if (@$options["redis"]) {
            self::initCache();
        }

        if (intval(@$options["relationsDepth"]) < 0) return [];
        // Definición de tablas y schemas
        $tables = array_filter(explode(",", $table));
        $tables = array_map("trim", $tables);
        $schemas = [];
        $fullSchemas = [];

        if (!@$options["ignoreSchema"]) {
            foreach ($tables as $index => $table) {
                $tableName = explode(" ", $table)[0];
                $schemaLoaded = loadSchema($tableName);
                if (empty($schemaLoaded)) {
                    unset($tables[$index]);
                    continue;
                }
                if (!$order) $order = $schemaLoaded["listPageOrder"];
                $fullSchemas[$tableName] = $schemaLoaded;

                $schemas[$tableName] = array_filter($schemaLoaded, function ($rec) use ($options) {
                    // Creo que esto es innecesario pero por si acaso se necesite el squema sin uploads si no se pide
                    if (@$options["uploads"]) {
                        return !empty($rec) && is_array($rec) && isset($rec["type"]) && $rec["type"] != "separator";
                    } else {
                        return !empty($rec) && is_array($rec) && isset($rec["type"]) && $rec["type"] != "separator" && $rec["type"] != "upload";
                    }
                });
            }
        }

        $select = @$options["onlyFields"] ?: ["*"];
        
        if (count($tables) > 1) {
            $select = [];
            foreach ($tables as $index => $table) {
                $referencia = @explode(" ", $table)[1];
                $tableName = @explode(" ", $table)[0];
                if (!isset($schemas[$tableName])) continue;

                if (!isset($referencia)) $referencia = $options["prefix"] . $tableName;
                if ($index === 0) {
                    $select[] = $referencia . ".*";
                } else {
                    $selectResult = $referencia . "." . join("," . $referencia . ".", array_map(function ($key) use ($referencia) {
                        return $key . " AS '" . $referencia . "." . $key . "'";
                    }, array_keys($schemas[$tableName])));
                    $select[] = $selectResult;
                }
            }
        }
        if (isset($options["aggregates"]) && is_array($options["aggregates"])) {
            foreach ($options["aggregates"] as $aggregate) {
                $select[] = $aggregate;
            }
        }

        // Definición de los FROM
        $from = join(",", array_map(function ($table) use ($options) {
            return $options["prefix"] . $table;
        }, $tables));

        // Definición del Where
        $where = self::parse_where($where, null);
        $meta_limit = 1000000;

        // Definición del Limit
        if ($limit && is_array($limit)) {
            if (isset($limit["perPage"])) $limit["limit"] = $limit["perPage"];
            if (!isset($limit["limit"])) self::error(["error" => "No se puede poner limit sin limit"]);
            if (isset($limit["page"]) && !isset($limit["offset"])) $limit["offset"] = (max(1, intval($limit["page"])) - 1) * (intval($limit["limit"]));
            $meta_limit = intval($limit["limit"]);
            if (isset($limit["offset"])) {
                $limit = intval($limit["offset"]) . "," . intval($limit["limit"]);
            } else {
                $limit = intval($limit["limit"]);
            }
        } else if ($limit) {
            if(strpos($limit, ',') !== false) {
                $meta_limit = intval(explode(',', $limit)[1]);
            } else {
                $meta_limit = intval($limit);
            }
        }


        $select = join(', ', $select);
        $sql = "SELECT " . $select;
        $sql .= " FROM " . $from;
        $sql .= $where ? " WHERE " . $where : "";
        $sql .= @$options["groupBy"] ? " GROUP BY " . $options["groupBy"] : "";
        $sql .= $order ? " ORDER BY " . $order : "";

        $query_without_limit = $sql;

        $sql .= $limit ? " LIMIT " . $limit : "";

        if (@$options['dieBeforeQuery']) {
            self::error(["info" => $sql]);
        }

        $hashSql = self::cacheGenerateHash(md5($sql . json_encode($options)));

        if (!self::$noCacheTABLES || (self::$noCacheTABLES && !in_array($TABLE_PREFIX . str_replace($TABLE_PREFIX, "", $table), self::$noCacheTABLES))) {
            if (@$options["redis"]) {
                $resultQueryRedis = self::cacheGet($hashSql);
                if (@$resultQueryRedis) {
                    return json_decode($resultQueryRedis, true);
                }
            } else if (self::$force_load_cache) {
                if (!empty(self::$getCaches[$hashSql])) {
                    return json_decode(self::$getCaches[$hashSql], true);
                }
            }
        }

        $num_rows = null;
        if (@$options["withMetas"]) {
            $countSql = "SELECT COUNT(*) AS totalRecords FROM " . $from;
            $countSql .= $where ? " WHERE " . $where : "";

            // Cuando hay GROUP BY, totalRecords debe contar grupos y no filas crudas.
            if (@$options["groupBy"]) {
                $countSql = "SELECT COUNT(*) AS totalRecords FROM (SELECT 1 FROM " . $from;
                $countSql .= $where ? " WHERE " . $where : "";
                $countSql .= " GROUP BY " . $options["groupBy"] . ") __count_groups";
            }

            $countResult = @mysql_query($countSql);
            if ($countResult) {
                $countRow = mysql_fetch_assoc($countResult);
                if (isset($countRow["totalRecords"])) $num_rows = intval($countRow["totalRecords"]);
            }

            // Fallback para no alterar comportamiento si el COUNT optimizado falla.
            if (is_null($num_rows)) {
                $num_rows = mysql_num_rows(mysql_query($query_without_limit));
            }
        }

        $resultQuery = mysql_query($sql) or self::error(["error" => "Error en la consulta SQL " . mysql_error()]);
        $records = [];

        $firstTable = explode(" ", $tables[0])[0];

        // Uploads cacheados
        if (@$options["uploads"] && self::$force_json_cache_uploads) {
            $uploadsResult = self::getUploadsResults($options);
            $uploadsResult[$firstTable] = isset($uploadsResult[$firstTable]) ? $uploadsResult[$firstTable] : [];
            $possible_keys_of_uploads = array_filter(array_keys(@$uploadsResult[$firstTable]));
        }

        // Records
        while ($record = mysql_fetch_assoc($resultQuery)) {
            // Uploads sin cache
            if (@$options["uploads"] && !self::$force_json_cache_uploads) {
                self::getUploadsResultsFromRecord($record, $firstTable, @$fullSchemas[$firstTable], $options);
            }
            if (!@$options["ignoreSchema"]) self::parseGetRecord($record, $firstTable, @$fullSchemas[$firstTable], $options, @$uploadsResult);


            if (@$options["returnDataByKey"]) {

                $records[$record[$options["returnDataByKey"]]] = $record;
            } else {

                $records[] = $record;
            }
        }

        if (self::$storeDebugData) {
            $debugData = [
                "hora" => date("Y-m-d H:i:s", time()) . " " . microtime(),
                "query" => $sql,
                "records" => $records,
                "time" => microtime(true) - $microtime
            ];

            if (self::$backTracePoint) {
                $debugData["backTracePoint"] = self::$backTracePoint;
            }

            self::$debugData[] = $debugData;
        }

        if (@$options["debug"]) {
            echo self::showDebug(true, count(self::$debugData) - 1);
        }

        if (@$options["withMetas"]) {
            $listDetails =  [
                "totalRecords" => $num_rows,
                //"totalMatches" => count($records),
                "perPage"      => $meta_limit,
                //"keyword"      => "",
                "totalPages"   => ceil($num_rows / max(1, $meta_limit)),
                //"page"         => 1,
                //"prevPage"     => 1,
                //"nextPage"     => 1
            ];
            if (@$options["redis"] && @$hashSql) {
                if (!self::$noCacheTABLES || (self::$noCacheTABLES && !in_array($TABLE_PREFIX . str_replace($TABLE_PREFIX, "", $table), self::$noCacheTABLES))) {
                    self::cacheSet($hashSql, json_encode([$listDetails, $records]), $options["redis_expire"]);
                }
            } else if (self::$force_load_cache) {
                self::$getCaches[$hashSql] = json_encode([$listDetails, $records]);
            }
            return [$listDetails, $records];
        }
        if (@$options["redis"] && @$hashSql) {
            if (!self::$noCacheTABLES || (self::$noCacheTABLES && !in_array($TABLE_PREFIX . str_replace($TABLE_PREFIX, "", $table), self::$noCacheTABLES))) {
                self::cacheSet($hashSql, json_encode($records), $options["redis_expire"]);
            }
        } else if (self::$force_load_cache) {
            self::$getCaches[$hashSql] = json_encode($records);
        }
        return $records;
    }

    /**
     * Fuerza que todas las consulta se ejecuten con caché redis
     */
    static function localCache()
    {
        self::$force_load_cache = true;
    }

    /**
     * Inicializa la caché
     * TO DO : Falta comprobar si no se puede conectar
     */
    static function initCache()
    {
        $redisHost = getenv('REDIS_HOST') ?: '127.0.0.1';
        $redisPort = (int)(getenv('REDIS_PORT') ?: 6379);

        if (!self::$redis) {
            self::$redis = new Redis();
            self::$redis->connect($redisHost, $redisPort);
        } else if (!self::$redis->isConnected()) {
            self::$redis->connect($redisHost, $redisPort);
        }
    }

    /**
     * Fuerza que todas las consulta se ejecuten con caché redis
     */
    static function fullCache($expireTime = 60)
    {
        self::$force_redis = true;
        self::$redis_expireTime = $expireTime;

        self::initCache();
    }
    /**
     * Genera un hash para esta web
     */
    static function cacheGenerateHash($string)
    {
        return $_SERVER["HTTP_HOST"] . "_" . $string . "_" . self::$redis_expireTime;
    }

    /**
     * Setea datos en el caché a través de un hash
     */
    static function cacheSet($hash, $data, $expireTime = null)
    {
        if (!self::$redis) return;
        if (!self::$redis->isConnected()) return;
        if (!$expireTime && self::$redis_expireTime) $expireTime = self::$redis_expireTime;
        if (!$expireTime) $expireTime = 60;
        self::$redis->set($hash, $data);
        self::$redis->expire($hash, $expireTime);
    }

    /**
     * Obtiene datos de el caché a través de un hash
     */
    static function cacheGet($hash)
    {
        if (!self::$redis) return;
        if (!self::$redis->isConnected()) return;
        // SE HA MODIFICADO ESTE SCRIPT PORQUE CONSUMIA MAS RECURSOS EL GET
        return self::$redis->exists($hash) ? self::$redis->get($hash) : null;
    }

    /**
     * Reajusta las variables de activación de cache según la url
     */
    static function bloquedCacheByURL($url)
    {
        if (!self::$noCacheURIS) return false;
        if (in_array($url, self::$noCacheURIS)) return true;
        foreach (self::$noCacheURIS as $noCacheURI) {
            if (preg_match('/' . $noCacheURI . '/i', $url, $matches, PREG_OFFSET_CAPTURE)) return true;
        }
        return false;
    }

    /**
     * Reemplaza el hook token en el html resultante para que la seguridad no bloquee las peticiones
     */
    static function replaceHooksToken($html)
    {
        session_start();
        $html = preg_replace_callback(
            "/var hooksToken(\s)?=(\s)?[\'\"]([a-zA-Z0-9]+)[\'\"]\;/i",
            function ($matches) {
                $token = sha1(session_id() . $_SERVER["HTTP_HOST"]);
                return "var hooksToken = '" . $token . "'; console.log('⚡️⚡️⚡️ Render cached HTML ⚡️⚡️⚡️');";
            },
            $html
        );
        return $html;
    }
    /**
     * Realiza una traducción recursiva de un array de valores
     */
    static function t_recursivo($record, $idx = null)
    {
        global $TABLE_PREFIX;
        if (is_null(self::$allowedTranslateFields)) {
            self::$allowedTranslateFields = array_flip(array_map(function ($field) {
                return $field['fieldName'];
            }, mysql_query_fetch_all_assoc("SELECT DISTINCT fieldName FROM {$TABLE_PREFIX}traducciones")));
        }
        $it = $idx ? $record[$idx] : $record;
        if (is_array($it)) {
            foreach ($it as $key => $value) {
                if (is_array($it[$key])) {
                    $it[$key] = self::t_recursivo($it[$key], null);
                } else {
                    if (isset(self::$allowedTranslateFields[$key])) {
                        $it[$key] = t($it, $key);
                        if (isset($it[$key . '_bd']) && strpos($it[$key], '[') === 0 && strpos($it[$key], ']') === (strlen($it[$key]) - 1)) {
                            $it[$key . '_bd'] = json_decode($it[$key], true);
                        }
                    }
                }
            }
            if ($idx) {
                $record[$idx] = $it;
            } else {
                $record = $it;
            }
        } else {
            if ($idx && isset(self::$allowedTranslateFields[$idx])) {
                $record[$idx] = t($record, $idx);
                if (isset($record[$idx . '_bd']) && strpos($record[$idx], '[') === 0 && strpos($record[$idx], ']') === (strlen($record[$idx]) - 1)) {
                    $record[$idx . '_bd'] = json_decode($record[$idx], true);
                }
            }
        }
        return $record;
    }
    /*
     * Función traida del CMS lib/menus/default/common.php
     */
    static function updateCategoryMetadata($tableName = null, $where = '')
    {
        global $escapedTableName, $schema, $TABLE_PREFIX;

        if (!$tableName) {
            $newEscapedTableName = $escapedTableName;
            $newSchema = $schema;
        } else {
            $newEscapedTableName = $TABLE_PREFIX . str_replace($TABLE_PREFIX, "", $tableName);
            $newSchema = loadSchema($newEscapedTableName);
        }
        if ($newSchema['menuType'] != 'category') {
            return;
        }

        // load categoriesByNum
        $categoriesByNum = array();

        if ($where) $where = " WHERE $where";

        $query = "SELECT * FROM $newEscapedTableName $where ORDER BY globalOrder";
        $result = mysql_query($query) or die("MySQL Error: " . mysql_error() . "\n");
        while ($row = mysql_fetch_assoc($result)) {
            $categoriesByNum[$row['num']] = $row;
        }
        if (is_resource($result)) {
            mysql_free_result($result);
        }

        // get childNums for each parentNum
        $childNumsOfParentNum = array();
        foreach (array_keys($categoriesByNum) as $num) {
            $parentNum = (int) $categoriesByNum[$num]['parentNum'];
            $childNumsOfParentNum[$parentNum][] = $num;
        }

        // reset order
        self::_updateCategoryBranch(array(
            'branchParent' => 0,
            'records'      => &$categoriesByNum,
            'childNodes'   => $childNumsOfParentNum,
        ));


        // save new order
        foreach ($categoriesByNum as $num => $category) {
            $query = "UPDATE `$newEscapedTableName` SET ";
            $query .= "`globalOrder`  = '" . mysql_real_escape_string($category['globalOrder']) . "', ";
            $query .= "`siblingOrder` = '" . mysql_real_escape_string($category['siblingOrder']) . "', ";
            $query .= "`depth`        = '" . mysql_real_escape_string($category['depth']) . "', ";
            $query .= "`lineage`      = '" . mysql_real_escape_string($category['lineage']) . "', ";
            $query .= "`breadcrumb`   = '" . mysql_real_escape_string($category['breadcrumb']) . "' ";
            $query .= "WHERE num = '{$category['num']}'";

            mysql_query($query) or die("There was an error updating the category metadata:\n\n" . htmlspecialchars(mysql_error()) . "\n");
        }
    }
    /*
     * Función traida del CMS lib/menus/default/common.php
     */
    static function _updateCategoryBranch($args)
    {

        ## set defaults
        if (!@$args['globalOrder']) {
            $args['globalOrder'] = 0;
        }
        if (!@$args['depth']) {
            $args['depth']       = 0;
        }
        if (!@$args['lineage']) {
            $args['lineage']     = ":";
        }

        # sort branch children
        $sortedChildren = array();
        foreach ($args['childNodes'][$args['branchParent']] as $childNum) {
            $sortedChildren[$childNum] = &$args['records'][$childNum];
        }
        uasort($sortedChildren, 'self::_sortCategoriesBySiblingOrder');

        # loop over branch children
        $siblingOrder = 0;
        foreach (array_keys($sortedChildren) as $childNum) {
            $childRecord = &$args['records'][$childNum];

            $childRecord['globalOrder']  = ++$args['globalOrder'];
            $childRecord['siblingOrder'] = ++$siblingOrder;
            $childRecord['depth']        = $args['depth'];
            $childRecord['lineage']      = $args['lineage'] . "$childNum:";
            $childRecord['breadcrumb']   = @$args['breadcrumb'] ? "{$args['breadcrumb']} : {$childRecord['name']}" : $childRecord['name'];

            # if child has children, loop over them
            if (@$args['childNodes'][$childNum]) {
                self::_updateCategoryBranch(array(
                    'branchParent' => $childNum,
                    'globalOrder'  => &$args['globalOrder'],
                    'records'      => &$args['records'],
                    'childNodes'   => $args['childNodes'],
                    'depth'        => ($args['depth'] + 1),
                    'lineage'      => $childRecord['lineage'],
                    'breadcrumb'   => $childRecord['breadcrumb'],
                ));
            }
        }
    }
    /*
     * Función traida del CMS lib/menus/default/common.php
     */
    static function _sortCategoriesBySiblingOrder($arrayA, $arrayB)
    {
        if ($arrayA['siblingOrder'] < $arrayB['siblingOrder']) {
            return -1;
        }
        if ($arrayA['siblingOrder'] > $arrayB['siblingOrder']) {
            return 1;
        }
        return 0;
    }
}
