<?php

namespace GriffinForms\Admin\Sql;

use GriffinForms\Traits\Singleton;

class Format extends \GriffinForms\Admin\Format
{
    use Singleton;
    
    protected $meta;
    protected $config;
    public $table;
    public $object;
    
    protected function setTable()
    {
        $this->table = $this->config->getTable($this->meta);
    }
    
    protected function getTable($type)
    {
        if ($type !== null) {
            return $this->config->getTable($type);
        } else {
            return $this->table;
        }
    }

    protected function checkCapabilities()
    {
        if(!(current_user_can('manage_options'))) {
            return;
        }
    }
    
    /**
     * Inserts a new record into the database.
     *
     * Validates input, JSON-encodes any array fields, performs the insert,
     * and logs an error if the insert fails.
     *
     * @param array $data Key-value pairs to insert.
     *
     * @return int|false Inserted item ID on success, or false on failure.
     */
    public function insertData($data)
    {
        global $wpdb;

        // Validate $data
        if (!is_array($data) || empty($data)) {
            return false; // Invalid or empty data provided
        }

        // Encode any array values before inserting
        foreach ($data as $key => $value) {
            if (is_array($value)) {
                $data[$key] = wp_json_encode($value);
            }
        }

        $insert = $wpdb->insert($this->table, $data);

        if ($insert === false && isset($this->log)) {
            $this->log->add(
                'error',       // log_type
                'insert',      // item_type
                0,             // item_id (unknown until inserted)
                get_current_user_id(), // user_id
                'Database insert failed: ' . $wpdb->last_error, // message
                'db'           // category
            );
        }

        return ($insert === false) ? false : $wpdb->insert_id;
    }
    
    /**
     * Updates a database record with the provided data.
     *
     * Validates input, JSON-encodes any array fields, performs the update,
     * and logs an error if the update fails.
     *
     * @param array $data  Key-value pairs to update.
     * @param array $where WHERE condition to find the record (typically by 'id').
     *
     * @return int|false Number of rows updated or false on failure.
     */
    public function updateData($data, $where)
    {
        global $wpdb;
    
        // Validate $data and $where
        if (!is_array($data) || empty($data)) {
            return false; // Invalid data provided
        }
    
        if (!is_array($where) || empty($where)) {
            return false; // No WHERE clause means dangerous query
        }
    
        // Encode any array values before updating
        foreach ($data as $key => $value) {
            if (is_array($value)) {
                $data[$key] = wp_json_encode($value);
            }
        }
    
        $update = $wpdb->update($this->table, $data, $where);
    
        // Log if update fails
        if ($update === false && isset($this->log)) {
            $this->log->add('error', 'update', 0, get_current_user_id(), 'Database update failed: ' . $wpdb->last_error, 'db');
        }
    
        return $update;
    }

    public function updateItemValue($type, $id, $name, $value)
    {
        $table = $this->config->getTable($type);

        if (!$table) {
            return false;
        }

        $data = array(
            $name => $value,
            'last_editor' => get_current_user_id(),
            'last_edited' => current_time('mysql', true)
        );
        
        
        $where = array('id'=> $id);

        global $wpdb;
        $update = $wpdb->update($table, $data, $where);
        return $update;
    }

    /**
     * Remove a child ID from a parent's serialized children column.
     *
     * Shared by delete cascade methods (formpage -> pagerow -> rowcolumn -> field).
     *
     * @param string $parent_type   Parent item type/table key.
     * @param int    $parent_id     Parent item ID.
     * @param string $children_type Parent column that stores child IDs.
     * @param int    $child_id      Child ID to remove from the list.
     * @return bool True when relation is synchronized or parent is unavailable.
     */
    protected function updateChildren($parent_type, $parent_id, $children_type, $child_id)
    {
        $parent_id = absint($parent_id);
        $child_id = absint($child_id);

        // When parent context is unavailable, nothing to sync.
        if ($parent_id < 1) {
            return true;
        }

        $children_ids_raw = $this->getItemValue($parent_id, $children_type, $parent_type);
        $children_ids = $this->maybeDecode($children_ids_raw);

        if (!is_array($children_ids)) {
            $children_ids = array();
        }

        $children_ids = array_values(array_filter(array_map('absint', $children_ids), static function ($id) {
            return $id > 0;
        }));

        $updated_children = array_values(array_filter($children_ids, static function ($id) use ($child_id) {
            return $id !== $child_id;
        }));

        $result = $this->updateItemValue($parent_type, $parent_id, $children_type, maybe_serialize($updated_children));
        return $result !== false;
    }
    
    public function itemExists($type, $id)
    {
        if (!is_numeric($id) or intval($id) < 1) {
            return false;
        }

        $table = $this->config->getTable(strtolower($type));

        if ($table === false) {
            return false;
        }

        global $wpdb;
        $row = $wpdb->get_row(
            $wpdb->prepare('SELECT * FROM %i WHERE id = %d', $table, intval($id))
        );
        if ($row == false) {
            return false;
        } else {
            return true;
        }
    }
    
    /**
     * Retrieves a single row or specific columns from the database by item ID.
     *
     * If no columns are specified, all columns (*) are retrieved.
     * You can optionally pass an array of specific column names to limit the result.
     *
     * Example usages:
     * - $this->itemValues(12); // returns full row
     * - $this->itemValues(12, 'message', ['name', 'heading']); // returns only name and heading
     *
     * @param int    $id      The item ID to fetch.
     * @param string $type    Optional. The item type to resolve the table name.
     * @param array  $columns Optional. Array of column names to retrieve. Defaults to all columns.
     *
     * @return object|null The retrieved row or null if not found.
     */
    public function itemValues($id, $type = null, $columns = array('*'))
    {
        $table = $this->getTable($type);
        global $wpdb;

        // Prepare columns for query
        $columns_sql = '*';
        if (is_array($columns) && !empty($columns) && $columns !== array('*')) {
            $columns_sql = implode(', ', array_map('esc_sql', $columns));
        }

        return $wpdb->get_row(
            $wpdb->prepare(
                "SELECT {$columns_sql} FROM %i WHERE id = %d",
                $table,
                $id
            )
        );
    }
    
/*     public function defaultValues($type)
    {
        global $wpdb;
        $table = esc_sql($this->config->getTable('defaults'));

        return $wpdb->get_results(
            $wpdb->prepare(
                "SELECT name, value FROM %i WHERE type = %s",
                $table, $type
            ),
            ARRAY_A
        );
    } */
    
    public function getColumns($scope = 'all')
    {
        global $wpdb;
        $fields = array();
        $columns = $wpdb->get_results($wpdb->prepare('SHOW COLUMNS FROM %i', esc_sql($this->table)), ARRAY_A);
        if ($scope != 'all') {
            foreach ($columns as $column) {
                array_push($fields, $column[$scope]);
            }
            $fields;
        } else {
            return $columns;
        }
    }
    
    public function deleteRow($id)
    {
        global $wpdb;
        $delete = $wpdb->delete($this->table, array('id'=>$id));
        return $delete;
    }
    
    // Need to phase out. Supersceeded by Options class.
    public function getOption($name)
    {
        global $wpdb;
        $table = $wpdb->prefix . 'griffinforms_options';
        $columns = $wpdb->get_var($wpdb->prepare('SELECT value FROM %i WHERE name=%s', $table, $name));
        return $this->maybeDecode($columns);
    }    
    
      // Need to phase out. Supersceeded by Options class.
    public function updateOption($name, $value)
    {
        global $wpdb;
        $table = $wpdb->prefix . 'griffinforms_options';
        $data = array('value' => maybe_serialize($value));
        $where = array('name' => $name);
        $update = $wpdb->update($table, $data, $where);
        return $update;
    }
    
    public function getItemValue($id, $column, $type = null) {
        global $wpdb;
        $table = $this->getTable($type);
        $value = $wpdb->get_var($wpdb->prepare('SELECT %i FROM %i WHERE id=%d', $column, $table, $id));
        return $value;
    }

    public function getFileNameFromToken($token)
    {
        $token = is_string($token) ? trim($token) : '';
        if ($token === '') {
            return '';
        }
        global $wpdb;
        $table = $this->config->getTable('files');
        $row = $wpdb->get_row(
            $wpdb->prepare("SELECT path, url FROM {$table} WHERE token = %s LIMIT 1", $token),
            ARRAY_A
        );
        if (!$row) {
            return '';
        }
        $candidate = '';
        if (!empty($row['path'])) {
            $candidate = wp_basename($row['path']);
        } elseif (!empty($row['url'])) {
            $parsed = wp_parse_url($row['url']);
            $candidate = isset($parsed['path']) ? wp_basename($parsed['path']) : '';
        }
        return (string) $candidate;
    }

    public function getSubmissionDisplayName($submission_id)
    {
        $submission_id = absint($submission_id);
        if ($submission_id <= 0) {
            return '';
        }

        $submission_data = $this->getItemValue($submission_id, 'submission', 'submission');
        $submission_data = $this->maybeDecode($submission_data);
        $display_name = $this->findSubmissionDisplayNameCandidate($submission_data);

        if ($display_name !== '') {
            return $display_name;
        }

        /* translators: %d is the submission ID number */
        return sprintf(__('Submission-%d', 'griffinforms-form-builder'), $submission_id);
    }

    /**
     * Walk the decoded submission payload and return the first useful label.
     *
     * Prefer human-readable text, skip raw URLs and numeric-only values, and
     * fall back to file names when a token-backed upload is encountered.
     *
     * @param mixed $submission_data Decoded submission payload.
     * @return string
     */
    protected function findSubmissionDisplayNameCandidate($submission_data)
    {
        if (!is_array($submission_data)) {
            return '';
        }

        foreach ($submission_data as $page) {
            $candidate = $this->findSubmissionDisplayNameCandidateInValue($page);
            if ($candidate !== '') {
                return $candidate;
            }
        }

        return '';
    }

    /**
     * Recursively inspect a submission value for a clean display candidate.
     *
     * @param mixed $value Raw submission value.
     * @return string
     */
    protected function findSubmissionDisplayNameCandidateInValue($value)
    {
        if (is_array($value)) {
            foreach ($value as $nested_value) {
                $candidate = $this->findSubmissionDisplayNameCandidateInValue($nested_value);
                if ($candidate !== '') {
                    return $candidate;
                }
            }

            return '';
        }

        return $this->normalizeSubmissionDisplayNameCandidate($value);
    }

    /**
     * Normalize a raw submission value into a readable display name.
     *
     * @param mixed $value Raw submission value.
     * @return string
     */
    protected function normalizeSubmissionDisplayNameCandidate($value)
    {
        if (!is_scalar($value)) {
            return '';
        }

        $raw_value = trim((string) $value);
        if ($raw_value === '') {
            return '';
        }

        $token = $this->extractTokenFromRaw($raw_value);
        if ($token !== '') {
            $filename = $this->getFileNameFromToken($token);
            if ($filename !== '') {
                return $filename;
            }

            return '';
        }

        if ($this->looksLikeLikertJsonValue($raw_value)) {
            return '';
        }

        if ($this->looksLikeOptionKeyArray($raw_value)) {
            return '';
        }

        if ($this->looksLikeTechnicalSubmissionValue($raw_value)) {
            return '';
        }

        $candidate = wp_strip_all_tags($raw_value, true);
        $candidate = html_entity_decode($candidate, ENT_QUOTES, get_bloginfo('charset'));
        $candidate = preg_replace('/\s+/u', ' ', $candidate);
        $candidate = trim((string) $candidate);

        if ($candidate === '' || $this->looksLikeTechnicalSubmissionValue($candidate)) {
            return '';
        }

        $max_length = 50;
        if (function_exists('mb_strlen') && function_exists('mb_substr')) {
            if (mb_strlen($candidate) > $max_length) {
                $candidate = mb_substr($candidate, 0, $max_length) . '...';
            }
        } elseif (strlen($candidate) > $max_length) {
            $candidate = substr($candidate, 0, $max_length) . '...';
        }

        return $candidate;
    }

    /**
     * Reject submission values that are likely to be technical artifacts.
     *
     * @param string $value Normalized submission value.
     * @return bool
     */
    protected function looksLikeTechnicalSubmissionValue($value)
    {
        $value = trim((string) $value);
        if ($value === '') {
            return true;
        }

        if (preg_match('/^(?:https?:)?\\/\\//i', $value) || preg_match('/^(?:data:|blob:)/i', $value)) {
            return true;
        }

        if (filter_var($value, FILTER_VALIDATE_URL)) {
            return true;
        }

        $collapsed = preg_replace('/[\\s\\p{P}]+/u', '', $value);
        if ($collapsed === '') {
            return true;
        }

        if (preg_match('/^\\d+$/', $collapsed)) {
            return true;
        }

        return false;
    }

    /**
     * Detect JSON-encoded option-key arrays produced by image selection and similar
     * choice fields (e.g. '["opt_261ed832","opt_9a3f1c00"]'). These are internal
     * identifiers and are never useful as a human-readable display name.
     *
     * @param string $value Raw submission value.
     * @return bool
     */
    protected function looksLikeOptionKeyArray($value)
    {
        $value = trim((string) $value);
        if ($value === '' || $value[0] !== '[') {
            return false;
        }

        $decoded = json_decode($value, true);
        if (json_last_error() !== JSON_ERROR_NONE || !is_array($decoded) || empty($decoded)) {
            return false;
        }

        foreach ($decoded as $item) {
            if (!is_string($item) || !preg_match('/^opt_[a-f0-9]+$/i', $item)) {
                return false;
            }
        }

        return true;
    }

    /**
     * Detect Likert-style JSON blobs that should never be used as a display name.
     *
     * @param string $value Normalized submission value.
     * @return bool
     */
    protected function looksLikeLikertJsonValue($value)
    {
        $value = trim((string) $value);
        if ($value === '') {
            return false;
        }

        if (($value[0] !== '{' && $value[0] !== '[') || strlen($value) < 2) {
            return false;
        }

        $decoded = json_decode($value, true);
        if (json_last_error() !== JSON_ERROR_NONE || !is_array($decoded)) {
            return false;
        }

        $json_keys = array_change_key_case(array_flip(array_keys($decoded)), CASE_LOWER);
        $likely_likert_keys = array(
            'likert',
            'likert_rows',
            'likert_columns',
            'rows',
            'columns',
            'row',
            'column',
            'answers',
            'response',
            'responses',
            'selected',
        );

        foreach ($likely_likert_keys as $key) {
            if (isset($json_keys[strtolower($key)])) {
                return true;
            }
        }

        if ($this->looksLikeLikertResponseMap($decoded)) {
            return true;
        }

        return false;
    }

    /**
     * Detect Likert-style row/column response maps such as:
     * {"row_10":"col_1","row_9":"col_2"}
     *
     * @param array $decoded JSON-decoded payload.
     * @return bool
     */
    protected function looksLikeLikertResponseMap(array $decoded)
    {
        if (empty($decoded)) {
            return false;
        }

        $row_like = 0;
        $col_like = 0;

        foreach ($decoded as $key => $value) {
            $key = strtolower(trim((string) $key));
            $value = strtolower(trim((string) $value));

            if (preg_match('/^row_[a-z0-9]+$/i', $key)) {
                $row_like++;
            }

            if (preg_match('/^col_[a-z0-9]+$/i', $value)) {
                $col_like++;
            }
        }

        $total = count($decoded);
        return $total > 0 && $row_like >= 1 && $col_like >= 1 && ($row_like + $col_like) >= $total;
    }

    protected function extractTokenFromRaw($raw)
    {
        if (!is_string($raw)) { return ''; }
        $raw = trim($raw);
        if ($raw === '') { return ''; }
        $first = explode(',', $raw)[0];
        $first = trim($first);
        return (preg_match('/^[a-f0-9]{40}$/i', $first)) ? $first : '';
    }

    public function getFormThemeName($form_id)
    {
        $form_id = absint($form_id);
        if (!$form_id) {
            return __('Default Styling', 'griffinforms-form-builder');
        }

        $theme_id = $this->getItemValue($form_id, 'form_theme', 'form');
        $theme_id = absint($theme_id);
        if (!$theme_id) {
            return __('Default Styling', 'griffinforms-form-builder');
        }

        $theme_name = $this->getItemValue($theme_id, 'name', 'formtheme');
        if (empty($theme_name)) {
            return __('Default Styling', 'griffinforms-form-builder');
        }

        return $theme_name;
    }

    public function getFormFolderName($form_id)
    {
        $form_id = absint($form_id);
        if (!$form_id) {
            return __('root', 'griffinforms-form-builder');
        }

        $folder_id = $this->getItemValue($form_id, 'folder', 'form');
        $folder_id = absint($folder_id);
        if (!$folder_id) {
            return __('root', 'griffinforms-form-builder');
        }

        $folder_name = $this->getItemValue($folder_id, 'name', 'folder');
        if (empty($folder_name)) {
            return __('root', 'griffinforms-form-builder');
        }

        return $folder_name;
    }
    
    public function getItems($type = null, $columns = array('*'), $include_drafts = false)
    {
        global $wpdb;
        $table = $this->getTable($type);
        $columns_sql = implode(', ', array_map('esc_sql', $columns));

        $query = "SELECT {$columns_sql} FROM %i";
        if (!$include_drafts && $this->hasColumn($table, 'status')) {
            $query .= " WHERE status != 'draft'";
        }

        return $wpdb->get_results(
            $wpdb->prepare($query, $table)
        );
    }
    
    public function getItemsForModal($type, $limit, $offset = 0, $include_drafts = false)
    {
        global $wpdb;
        $table = $this->config->getTable($type);
        
        $query = "SELECT * FROM %i";
        if (!$include_drafts && $this->hasColumn($table, 'status')) {
            $query .= " WHERE status != 'draft'";
        }
        $query .= " ORDER BY id DESC LIMIT %d OFFSET %d";

        return $wpdb->get_results(
            $wpdb->prepare(
                $query,
                $table,
                (int) $limit,
                (int) $offset
            )
        );
    }
    
    public function getItemCount($type)
    {
        global $wpdb;
        $table = $this->config->getTable($type);
        $count = $wpdb->get_var(
            $wpdb->prepare(
                'SELECT COUNT(id) AS ItemCount FROM %i',
                $table
                )
            );
        return $count;
    }    
    
    public function getSearchedItems($type, $search = '', $include_drafts = false)
    {
        global $wpdb;
        $table = $this->config->getTable($type);
        $search_term = '%' . $wpdb->esc_like($search) . '%';
        
        $query = "SELECT * FROM %i WHERE (name LIKE %s OR heading LIKE %s OR description LIKE %s)";
        if (!$include_drafts && $this->hasColumn($table, 'status')) {
            $query .= " AND status != 'draft'";
        }
        $query .= " ORDER BY id DESC";

        return $wpdb->get_results(
            $wpdb->prepare(
                $query,
                $table,
                $search_term,
                $search_term,
                $search_term
            )
        );
    }
    
    public function getLatestItemId()
    {
        global $wpdb;
        return $wpdb->get_var($wpdb->prepare('SELECT id FROM %i ORDER BY id DESC LIMIT 1', $this->table));
    }
    
    public function getLatestFormElementId($item_type)
    {
        global $wpdb;
        $table = $this->config->getTable($item_type);
        return $wpdb->get_var(
            $wpdb->prepare(
                'SELECT id FROM %i ORDER BY id DESC LIMIT 1',
                $table
            )
        );
    }

    public function createFormElement($item_type, $item_data, $format)
    {
        global $wpdb;
        $table = $this->config->getTable($item_type);
        $insert = $wpdb->insert($table, $item_data, $format);
        
        if ($insert == 'false') {
            return false;
        } else {
            return $wpdb->insert_id;
        }
    }
    
    public function getDbTableList()
    {
        global $wpdb;
        return $wpdb->tables('all', true);
    }

    protected function itemCreateData()
    {
        $data = array(
            'author' => get_current_user_id(),
            'created' => current_time('mysql', true)
        );
        
        return $data;
    }
    
    public function pruneItems($item_type, $item_ids)
    {
        global $wpdb;
        $table = $this->config->getTable($item_type);
        $valid_ids = $wpdb->get_col($wpdb->prepare('SELECT id FROM %i', $table));
        return array_intersect($item_ids, $valid_ids);
    }

    public function deleteItemById($type, $id) {
        global $wpdb;
        $table = $this->config->getTable($type);
        $where = array('id' => $id);
        $where_format = array('%d');
        return $wpdb->delete($table, $where, $where_format);
    }

    public function deleteForm($id, $delete_submissions = false)
    {
        $id = absint($id);
    
        if ($this->itemExists('form', $id)) {
            $form = $this->itemValues($id, 'form');
            $page_ids = $this->maybeDecode($form->formpages);
            
            if (is_array($page_ids)) {
                foreach ($page_ids as $page_id) {
                    $this->deleteFormPage($page_id, $id);
                }
            }
    
            // Delete submissions if the user requested
            if ($delete_submissions) {
                global $wpdb;
                $submission_table = $this->config->getTable('submission');
                $wpdb->delete($submission_table, array('form_id' => $id));
                \GriffinForms\Includes\Pipelines\SubmissionHistoryManager::getInstance()->deleteOrphanedRevisions();
            }
    
            return $this->deleteItemById('form', $id);
    
        } else {
            return false;
        }
    }

    public function deleteFormPage($id, $form_id)
    {
        $id = absint($id);

        if ($this->itemExists('formpage', $id)) {
            $formpage = $this->itemValues($id, 'formpage');
            $row_ids = $this->maybeDecode($formpage->pagerows);
            
            if (is_array($row_ids)) {
                foreach ($row_ids as $row_id) {
                    $this->deletePageRow($row_id, $id);
                }
            }

            $delete = $this->deleteItemById('formpage', $id);
            $this->updateChildren('form', $form_id, 'formpages', $id);
            return $delete;

        } else {
            return false;
        }
    }

    public function deletePageRow($id, $page_id)
    {
        $id = absint($id);

        if ($this->itemExists('pagerow', $id)) {
            $row = $this->itemValues($id, 'pagerow');
            $column_ids = $this->maybeDecode($row->rowcolumns);
            
            if (is_array($column_ids)) {
                foreach ($column_ids as $column_id) {
                    $this->deleteRowColumn($column_id, $id);
                }
            }

            $delete = $this->deleteItemById('pagerow', $id);
            $this->updateChildren('formpage', $page_id, 'pagerows', $id);
            return $delete;

        } else {
            return false;
        }
    }

    public function deleteRowColumn($id, $row_id)
    {
        $this->checkCapabilities();

        $id = absint($id);

        if ($this->itemExists('rowcolumn', $id)) {
            $column = $this->itemValues($id, 'rowcolumn');
            $field_ids = $this->maybeDecode($column->fields);
            
            if (is_array($field_ids)) {
                foreach ($field_ids as $field_id) {
                    if ($this->itemExists('field', $field_id)) {
                        $field = $this->itemValues($field_id, 'field');
                        if (!$field->is_reusable) {
                            $this->deleteField($field_id, $id);
                        }
                    }
                }
            }

            $delete = $this->deleteItemById('rowcolumn', $id);
            $this->updateChildren('pagerow', $row_id, 'rowcolumns', $id);
            return $delete;

        } else {
            return false;
        }
    }

    public function deleteField($id, $column_id)
    {
        $this->checkCapabilities();
        $id = absint($id);
        $delete = $this->deleteItemById('field', $id);
        $this->updateChildren('rowcolumn', $column_id, 'fields', $id);
        return $delete;
    }

    /**
     * Removes a specific field ID from a column's field list.
     *
     * This is typically used when detaching a reusable field from a row-column layout.
     *
     * @param int $column_id The ID of the column containing the field.
     * @param int $field_id  The field ID to remove.
     * @return int|false Number of rows updated or false on failure.
     */
    public function removeField($column_id, $field_id)
    {
        $this->checkCapabilities();

        $table = $this->config->getTable('rowcolumn');
        $fields = $this->maybeDecode($this->getItemValue($column_id, 'fields', 'rowcolumn'));

        if (!is_array($fields)) {
            return false;
        }

        // Filter out the field ID from the current field list
        $fields = array_filter($fields, function ($id) use ($field_id) {
            return (int)$id !== (int)$field_id;
        });

        global $wpdb;
        return $wpdb->update(
            $table,
            ['fields' => maybe_serialize(array_values($fields))],
            ['id' => $column_id],
            ['%s'],
            ['%d']
        );
    }

    protected function hasColumn(string $table, string $column): bool
    {
        $columns = $this->getTableColumns($table);
        return isset($columns[$column]);
    }

    /**
     * Cache table columns per request so repeated format queries do not keep
     * hitting INFORMATION_SCHEMA on every page load.
     *
     * @return array<string, true>
     */
    private function getTableColumns(string $table): array
    {
        static $cache = array();
        if (array_key_exists($table, $cache)) {
            return $cache[$table];
        }

        global $wpdb;
        $columns = $wpdb->get_col($wpdb->prepare("SHOW COLUMNS FROM %i", $table), 0);
        if (!is_array($columns)) {
            $columns = array();
        }

        $cache[$table] = array_fill_keys(array_map('strval', $columns), true);
        return $cache[$table];
    }
}
