// ----- Начало файла: ./mvc-js\app.js -----
// app.js - Точка входа приложения

import { Controller } from './controller.js';
import { Model } from './model.js';
import { View } from './view.js';

document.addEventListener("DOMContentLoaded", () => {
    const model = new Model();
    const view = new View();
    const controller = new Controller(model, view);
    controller.initialize();
});


// ----- Начало файла: ./mvc-js\controller.js -----
// controller.js - Обработка взаимодействия пользователя и координация между Model и View

import { getTranslatedName } from './utils.js';
import i18next from 'https://cdn.skypack.dev/i18next@21.6.16';
import HttpBackend from 'https://cdn.skypack.dev/i18next-http-backend@1.4.0';
import { debounce } from './utils.js';

export class Controller {
    constructor(model, view) {
        this.model = model;
        this.view = view;

        // Привязка методов
        this.handleViewData = this.handleViewData.bind(this);
        this.onLanguageChange = this.onLanguageChange.bind(this);
        this.setupInfiniteScroll = this.setupInfiniteScroll.bind(this);    
        
        // Инициализация флага
        this.toggleHandlerAdded = false;
    }

    async initialize() {
        try {
            // Инициализация i18next
            await i18next
                .use(HttpBackend)
                .init({
                    fallbackLng: 'en',
                    debug: false,
                    supportedLngs: ['en', 'ru', 'zh'], // Добавьте все поддерживаемые языки здесь
                    backend: {
                        loadPath: '../locales/{{lng}}/translations.json'
                    }
                });
    
            const currentLang = localStorage.getItem('currentLanguage') || navigator.language.split('-')[0] || 'en';
            await i18next.changeLanguage(currentLang);
    
            // Установка текущего языка в селектор
            const languageSelect = document.getElementById('language-select');
            languageSelect.value = i18next.language || 'en';

            // Инициализация модели (IndexedDB)
            await this.model.initialize();

            // Передача карты категорий в View
            this.view.initializeCategoriesMap(this.model.getCategoriesMap());

            // Обновление контента
            this.view.updateContent();

            // Заполнение фильтров
            await this.populateFilters();

            // Назначение обработчиков событий
            this.setupEventListeners();

            // Обработка начального отображения данных
            await this.handleViewData();

            // Настройка Infinite Scroll
            this.setupInfiniteScroll();
    
            console.log("Приложение инициализировано успешно.");
        } catch (error) {
            console.error("Произошла ошибка при инициализации приложения.", error);
            this.view.showError("errorInitializingApp");
        }
    }

    async populateFilters() {
        const currentLang = i18next.language || 'en';
        const colors = await this.model.getAllRecords("colors");
        const types = await this.model.getAllRecords("types");
        const languages = await this.model.getAllRecords("languages");
    
        // Получаем список всех доступных языков интерфейса
        const supportedLanguages = i18next.options.supportedLngs || ['en'];
    
        // Фильтруем языки, исключая те, которые уже есть в интерфейсе
        const filteredLanguages = languages.filter(lang => !supportedLanguages.includes(lang.id));
    
        // Получаем все фильтрованные флаги и карту имен
        const { filteredFlags, flagNamesMap } = await this.model.fetchFilteredFlags("");
    
        // Инициализация счётчиков
        const colorCounts = {};
        const typeCounts = {};
        const languageCounts = {};
    
        colors.forEach(color => colorCounts[color.id] = 0);
        types.forEach(type => typeCounts[type.id] = 0);
        filteredLanguages.forEach(lang => languageCounts[lang.id] = 0);
    
        // Одним проходом посчитаем соответствия
        filteredFlags.forEach(flag => {
            // Подсчет цветов
            flag.colors.forEach(colorId => {
                if (colorCounts.hasOwnProperty(colorId)) {
                    colorCounts[colorId]++;
                }
            });
    
            // Подсчет типов
            flag.types.forEach(typeId => {
                if (typeCounts.hasOwnProperty(typeId)) {
                    typeCounts[typeId]++;
                }
            });
    
            // Подсчет языков
            const namesForFlag = flagNamesMap[flag.id] || [];
            const uniqueLanguages = new Set();
            namesForFlag.forEach(nameObj => {
                if (filteredLanguages.some(lang => lang.id === nameObj.language_id)) {
                    uniqueLanguages.add(nameObj.language_id);
                }
            });
            uniqueLanguages.forEach(langId => {
                if (languageCounts.hasOwnProperty(langId)) {
                    languageCounts[langId]++;
                }
            });
        });
    
        // Отладочные сообщения
        console.log("Color Counts:", colorCounts);
        console.log("Type Counts:", typeCounts);
        console.log("Language Counts:", languageCounts);
    
        // Рендеринг фильтров с учётом подсчётов
        this.renderColorFilters(colors, colorCounts);
        this.renderTypeFilters(types, typeCounts);
        this.renderLanguageFilters(filteredLanguages, languageCounts);
    
        // Обновление контента после рендеринга фильтров
        this.view.updateContent();
    }

    renderColorFilters(colors, colorCounts) {
        const colorFiltersDiv = document.getElementById("color-filters");
        if (!colorFiltersDiv) {
            console.error("Не найден элемент с ID 'color-filters'");
            return;
        }
        colorFiltersDiv.innerHTML = ""; // Очистка перед заполнением

        colors.sort((a, b) => parseInt(a.id) - parseInt(b.id)); // ID сортируются как числа, а не строки
        colors.forEach(color => {
            const label = document.createElement("label");
            label.classList.add("filter-option");
            label.style.display = "flex";
            label.style.alignItems = "center";

            const checkbox = document.createElement("input");
            checkbox.type = "checkbox";
            checkbox.value = color.id;
            checkbox.setAttribute('aria-label', getTranslatedName(color));

            const colorSquare = document.createElement("div");
            colorSquare.style.width = "15px";
            colorSquare.style.height = "15px";
            colorSquare.style.backgroundColor = color.hex;
            colorSquare.style.margin = "0 5px";
            colorSquare.style.border = "1px solid #000";

            const colorName = document.createElement("span");
            // Отображаем количество флагов рядом с именем цвета
            colorName.textContent = `${getTranslatedName(color)} (${colorCounts[color.id] || 0})`;

            label.appendChild(checkbox);
            label.appendChild(colorSquare);
            label.appendChild(colorName);

            colorFiltersDiv.appendChild(label);
        });
    }

    renderTypeFilters(types, typeCounts) {
        const typeFiltersDiv = document.getElementById("type-filters");
        if (!typeFiltersDiv) {
            console.error("Не найден элемент с ID 'type-filters'");
            return;
        }
        typeFiltersDiv.innerHTML = ""; // Очистка перед заполнением

        types.sort((a, b) => parseInt(a.id) - parseInt(b.id)); // ID сортируются как числа, а не строки
        types.forEach(type => {
            const label = document.createElement("label");
            label.classList.add("filter-option");
            label.innerHTML = `
                <input type="checkbox" value="${type.id}" /> ${getTranslatedName(type)} (${typeCounts[type.id] || 0})
            `;
            label.setAttribute('aria-label', getTranslatedName(type));
            typeFiltersDiv.appendChild(label);
        });

        this.view.updateContent();
    }

    renderLanguageFilters(languages, languageCounts) {
        const languageFiltersDiv = document.getElementById("language-filters");
        if (!languageFiltersDiv) {
            console.error("Не найден элемент с ID 'language-filters'");
            return;
        }
        languageFiltersDiv.innerHTML = ""; // Очистка перед заполнением

        languages.sort((a, b) => getTranslatedName(a).localeCompare(getTranslatedName(b)));
        languages.forEach(language => {
            const label = document.createElement("label");
            label.classList.add("filter-option");
            label.innerHTML = `
                <input type="checkbox" value="${language.id}" /> ${getTranslatedName(language)} (${languageCounts[language.id] || 0})
            `;
            label.setAttribute('aria-label', getTranslatedName(language));
            languageFiltersDiv.appendChild(label);
        });
    }

    setupEventListeners() {
        // Фильтры по цветам
        const colorCheckboxes = document.querySelectorAll("#color-filters input[type='checkbox']");
        colorCheckboxes.forEach(checkbox => {
            checkbox.addEventListener("change", (event) => {
                if (event.target.checked) {
                    this.model.getSelectedFilters().colors.add(event.target.value);
                } else {
                    this.model.getSelectedFilters().colors.delete(event.target.value);
                }
                this.handleViewData();
            });
        });

        // Фильтры по типам
        const typeCheckboxes = document.querySelectorAll("#type-filters input[type='checkbox']");
        typeCheckboxes.forEach(checkbox => {
            checkbox.addEventListener("change", (event) => {
                if (event.target.checked) {
                    this.model.getSelectedFilters().types.add(event.target.value);
                } else {
                    this.model.getSelectedFilters().types.delete(event.target.value);
                }
                this.handleViewData();
            });
        });

        // Фильтры по языкам
        const languageCheckboxes = document.querySelectorAll("#language-filters input[type='checkbox']");
        languageCheckboxes.forEach(checkbox => {
            checkbox.addEventListener("change", (event) => {
                if (event.target.checked) {
                    this.model.getSelectedFilters().languages.add(event.target.value);
                } else {
                    this.model.getSelectedFilters().languages.delete(event.target.value);
                }
                this.handleViewData();
            });
        });

        // Кнопка Поиска
        const searchButton = document.getElementById("search-button");
        if (searchButton) {
            searchButton.addEventListener("click", this.handleViewData);
        }

        // Кнопка Очистки Поиска
        const clearSearchButton = document.getElementById("clear-search");
        if (clearSearchButton) {
            clearSearchButton.addEventListener("click", () => {
                const searchInput = document.getElementById("search-input");
                if (searchInput) {
                    searchInput.value = "";
                }
                this.handleViewData();
            });
        }

        // Кнопка Очистки Фильтров
        const clearFiltersButton = document.getElementById("clear-filters");
        if (clearFiltersButton) {
            clearFiltersButton.addEventListener("click", () => {
                // Снимаем все чекбоксы
                const allCheckboxes = document.querySelectorAll("#filters-panel input[type='checkbox']");
                allCheckboxes.forEach(checkbox => {
                    checkbox.checked = false;
                });

                // Очищаем выбранные фильтры
                this.model.getSelectedFilters().colors.clear();
                this.model.getSelectedFilters().types.clear();
                this.model.getSelectedFilters().languages.clear();

                this.handleViewData();
            });
        }

        // Обработчик изменения языка
        const languageSelect = document.getElementById('language-select');
        if (languageSelect) {
            languageSelect.addEventListener('change', this.onLanguageChange);
        }

        // Фильтрация в реальном времени (поиск)
        const searchInput = document.getElementById("search-input");
        if (searchInput) {
            searchInput.addEventListener("input", debounce(() => {
                this.model.getPaginationState().currentPage = 1;
                this.model.getPaginationState().hasMore = true;
                this.handleViewData();
            }, 300));
        }

        // Убедитесь, что обработчик для toggle-фильтров устанавливается только один раз
        const toggleButton = document.getElementById("toggle-filters");
        const filtersPanel = document.getElementById("filters-panel");

        if (toggleButton && filtersPanel && !this.toggleHandlerAdded) {
            toggleButton.addEventListener("click", () => {
                filtersPanel.classList.toggle("hidden");
            });
            this.toggleHandlerAdded = true; // Флаг, чтобы обработчик не добавлялся повторно
        }
    }

    /**
     * Присваивает подразделения каждому флагу
     * @param {Array} flags - Массив флагов
     * @param {object} flagNamesMap - Карта названий флагов
     * @returns {Promise<Array>}
     */
    async attachSubdivisionsToFlags(flags, flagNamesMap) {
        // Получаем массив промисов для получения подразделений каждого флага
        const subdivisionsPromises = flags.map(async (flag) => {
            // Получаем подразделения, где parent_flag_id == flag.id
            const subdivisions = await this.model.getFlagSubdivisions(flag.id);
            flag.subdivisions = subdivisions || [];
            return flag;
        });

        // Ждём завершения всех промисов
        const flagsWithSubdivisions = await Promise.all(subdivisionsPromises);
        return flagsWithSubdivisions;
    }

    async handleViewData() {
        try {
            this.view.showLoading();
            // Сброс пагинации
            this.model.getPaginationState().currentPage = 1;
            this.model.getPaginationState().hasMore = true;
            const searchQuery = document.getElementById("search-input").value.trim();
            const { filteredFlags, flagNamesMap } = await this.model.fetchFilteredFlags(searchQuery);
            const pagination = this.model.getPaginationState();
            pagination.filteredFlags = filteredFlags;
            pagination.flagNamesMap = flagNamesMap;

            // Получаем флаги для текущей страницы
            const flagsForPage = this.getFlagsForPage(pagination.currentPage, pagination.pageSize, filteredFlags);

            // Получаем подразделения для этих флагов
            const flagsWithSubdivisions = await this.attachSubdivisionsToFlags(flagsForPage, flagNamesMap);

            // Рендерим флаги с подразделениями
            this.view.renderFlags(
                flagsWithSubdivisions,
                flagNamesMap,
                pagination.currentPage,
                pagination.pageSize,
                this.model.getLanguagesMap(),
                this.model.getColorsMap(),
                this.model.getCategoriesMap() // Передаём карту категорий
            );

            this.view.hideLoading();
        } catch (error) {
            console.error("Ошибка при отображении данных.", error);
            this.view.hideLoading();
            this.view.showError("errorLoadingFlags");
        }
    }

    /**
     * Получает флаги для указанной страницы
     * @param {number} page - Номер страницы
     * @param {number} pageSize - Размер страницы
     * @param {Array} filteredFlags - Отфильтрованные флаги
     * @returns {Array}
     */
    getFlagsForPage(page, pageSize, filteredFlags) {
        const startIndex = (page - 1) * pageSize;
        const endIndex = startIndex + pageSize;
        return filteredFlags.slice(startIndex, endIndex);
    }

    /**
     * Заполняет карту категорий в модели
     */
    async populateCategoriesMap() {
        const categories = await this.model.getAllRecords("categories");
        this.model.setCategoriesMap(categories);
    }

    async onLanguageChange(event) {
        const selectedLang = event.target.value;
        try {
            await i18next.changeLanguage(selectedLang);
            localStorage.setItem('currentLanguage', selectedLang); // Сохраняем текущий язык в localStorage
            this.view.updateContent();
            await this.populateFilters();

            // Обновление карты категорий после смены языка
            this.view.initializeCategoriesMap(this.model.getCategoriesMap());

            // Переустанавливаем обработчики событий после обновления фильтров
            this.setupEventListeners();
            this.handleViewData();
        } catch (error) {
            console.error("Ошибка при смене языка.", error);
            this.view.showError("errorChangingLanguage");
        }
    }

    setupInfiniteScroll() {
        window.addEventListener("scroll", debounce(async () => {
            const { isLoading, hasMore, currentPage, pageSize, filteredFlags, flagNamesMap } = this.model.getPaginationState();

            if (isLoading || !hasMore) return;

            if ((currentPage * pageSize) >= filteredFlags.length) {
                this.model.getPaginationState().hasMore = false;
                return;
            }

            if (window.innerHeight + window.scrollY >= document.body.offsetHeight - 500) {
                this.model.getPaginationState().isLoading = true;
                this.model.getPaginationState().currentPage += 1;

                // Получаем флаги для новой страницы
                const flagsForPage = this.getFlagsForPage(currentPage, pageSize, filteredFlags);

                // Получаем подразделения для этих флагов
                const flagsWithSubdivisions = await this.attachSubdivisionsToFlags(flagsForPage, flagNamesMap);

                this.view.renderFlags(
                    flagsWithSubdivisions,
                    flagNamesMap, // Используем сохранённый map
                    currentPage,
                    pageSize,
                    this.model.getLanguagesMap(),
                    this.model.getColorsMap(),
                    this.model.getCategoriesMap() // Передаём карту категорий
                );

                this.model.getPaginationState().isLoading = false;

                if ((currentPage * pageSize) >= filteredFlags.length) {
                    this.model.getPaginationState().hasMore = false;
                }
            }
        }, 200), { passive: true });
    }
}

// ----- Начало файла: ./mvc-js\db.js -----
// db.js - Взаимодействие с IndexedDB

import { languageData } from './data/languageData.js';
import { typeData } from './data/typeData.js';
import { colorData } from './data/colorData.js';
import { categoryData } from './data/categoryData.js';
import { flagNameData } from './data/flagNameData.js';
import { flagData } from './data/flagData.js';
import { flagSubdivisionsData } from './data/flagSubdivisionsData.js';
import { flagSubdivisionNamesData } from './data/flagSubdivisionNamesData.js';

const dataMapping = {
    languages: {
        data: languageData,
        storeName: "languages",
        keyPath: "id",
        fields: ["id", "ru_name", "en_name", "zh_name"]
    },
    types: {
        data: typeData,
        storeName: "types",
        keyPath: "id",
        fields: ["id", "ru_name", "en_name", "zh_name"]
    },
    colors: {
        data: colorData,
        storeName: "colors",
        keyPath: "id",
        fields: ["id", "ru_name", "en_name", "zh_name", "hex"]
    },
    categories: {
        data: categoryData,
        storeName: "categories",
        keyPath: "id",
        fields: ["id", "ru_name", "en_name", "zh_name"]
    },
    flagNames: {
        data: flagNameData,
        storeName: "flagNames",
        keyPath: ["flag_id", "language_id"], // Составной ключ
        fields: ["flag_id", "language_id", "short_name", "long_name"]
    },
    flags: {
        data: flagData,
        storeName: "flags",
        keyPath: "id",
        fields: ["id", "is_index_ISO", "resolution", "resolution_ratio", "colors", "types", "originalNames", "parent_flag_id"]
    },
    flagSubdivisions: {
        data: flagSubdivisionsData,
        storeName: "flagSubdivisions",
        keyPath: "id",
        fields: ["category_id", "id", "parent_flag_id"]
    },
    flagSubdivisionNames: {
        data: flagSubdivisionNamesData,
        storeName: "flagSubdivisionNames",
        keyPath: "id",
        fields: ["id", "originalNames", "en_name", "ru_name", "zh_name"]
    },
    // Добавьте больше соответствий при необходимости
};

/**
 * Инициализирует IndexedDB и загружает данные при необходимости
 * @returns {Promise<IDBDatabase>}
 */
export function initDB() {
    return new Promise((resolve, reject) => {
        const request = indexedDB.open("FlagsDatabase", 1);

        request.onupgradeneeded = function(event) {
            const db = event.target.result;

            // Создаём объектные хранилища и индексы
            Object.values(dataMapping).forEach(config => {
                if (!db.objectStoreNames.contains(config.storeName)) {
                    if (Array.isArray(config.keyPath)) {
                        const store = db.createObjectStore(config.storeName, { keyPath: config.keyPath });
                        // Создаём индексы, если необходимо
                        if (config.storeName === "flagNames") {
                            store.createIndex("flag_id", "flag_id", { unique: false });
                            store.createIndex("language_id", "language_id", { unique: false });
                        }
                        // Добавьте другие индексы при необходимости
                    } else {
                        const store = db.createObjectStore(config.storeName, { keyPath: config.keyPath });
                        // Создаём индексы
                        if (config.storeName === "languages") {
                            store.createIndex("en_name", "en_name", { unique: false });
                            store.createIndex("ru_name", "ru_name", { unique: false });
                            store.createIndex("zh_name", "zh_name", { unique: false });
                        } else if (config.storeName === "flags") {
                            store.createIndex("types", "types", { unique: false, multiEntry: true });
                            store.createIndex("colors", "colors", { unique: false, multiEntry: true });
                            store.createIndex("parent_flag_id", "parent_flag_id", { unique: false });
                        } else if (config.storeName === "flagSubdivisions") {
                            store.createIndex("parent_flag_id", "parent_flag_id", { unique: false });
                        }
                        // Добавьте другие индексы для других хранилищ при необходимости
                    }
                }
            });
        };

        request.onsuccess = function(event) {
            const db = event.target.result;
            console.log("База данных успешно открыта");
            resolve(db);
        };

        request.onerror = function(event) {
            console.error("Не удалось открыть базу данных", event.target.error);
            reject(event.target.error);
        };
    });
}

/**
 * Добавляет данные в указанное объектное хранилище
 * @param {IDBDatabase} db - Открытая база данных
 * @param {string} storeName - Имя объектного хранилища
 * @param {Array} dataArray - Массив данных для добавления
 * @param {string|Array} keyPath - Ключевое поле или составной ключ
 * @returns {Promise<void>}
 */
export function addData(db, storeName, dataArray, keyPath) {
    return new Promise((resolve, reject) => {
        const transaction = db.transaction([storeName], "readwrite");
        const store = transaction.objectStore(storeName);

        transaction.oncomplete = function() {
            console.log(`Все данные добавлены в ${storeName}`);
            resolve();
        };

        transaction.onerror = function(event) {
            console.error(`Ошибка при добавлении данных в ${storeName}:`, event.target.error);
            reject(event.target.error);
        };

        dataArray.forEach(item => store.put(item));
    });
}

/**
 * Получает количество записей в указанном объектном хранилище
 * @param {IDBDatabase} db - Открытая база данных
 * @param {string} storeName - Имя объектного хранилища
 * @returns {Promise<number>}
 */
export function getCount(db, storeName) {
    return new Promise((resolve, reject) => {
        const transaction = db.transaction([storeName], "readonly");
        const store = transaction.objectStore(storeName);
        const request = store.count();

        request.onsuccess = function(event) {
            resolve(event.target.result);
        };

        request.onerror = function(event) {
            console.error(`Ошибка при подсчёте записей в ${storeName}:`, event.target.error);
            reject(event.target.error);
        };
    });
}

/**
 * Загружает все данные в IndexedDB при необходимости
 * Данные загружаются только при первом создании базы данных
 * @param {IDBDatabase} db - Открытая база данных
 * @returns {Promise<void>}
 */
export async function loadData(db) {
    try {
        // Проверяем, пусто ли объектное хранилище "languages"
        const countLanguages = await getCount(db, "languages");
        console.log(`Количество записей в 'languages': ${countLanguages}`);

        if (countLanguages === 0) {
            for (const [key, config] of Object.entries(dataMapping)) {
                console.log(`Добавление данных в хранилище: ${config.storeName}`);
                await addData(db, config.storeName, config.data, config.keyPath);
                console.log(`Данные добавлены в хранилище: ${config.storeName}`);
            }
            console.log("Все данные успешно загружены в IndexedDB.");
        } else {
            console.log("Данные уже существуют в базе данных. Загрузка не требуется.");
        }
    } catch (error) {
        console.error("Произошла ошибка при загрузке данных. Пожалуйста, попробуйте позже.", error);
    }
}

/**
 * Получает все записи из указанного объектного хранилища
 * @param {IDBDatabase} db - Открытая IndexedDB база данных
 * @param {string} storeName - Имя объектного хранилища
 * @returns {Promise<Array>}
 */
export function getAllRecords(db, storeName) {
    return new Promise((resolve, reject) => {
        const transaction = db.transaction([storeName], "readonly");
        const store = transaction.objectStore(storeName);
        const request = store.getAll();

        request.onsuccess = function(event) {
            resolve(event.target.result);
        };

        request.onerror = function(event) {
            console.error(`Ошибка при получении данных из ${storeName}:`, event.target.error);
            reject(event.target.error);
        };
    });
}


/**
 * Получает подразделения флага
 * @param {IDBDatabase} db - Открытая база данных
 * @param {string} parentFlagId - ID родительского флага
 * @returns {Promise<Array>} - Массив подразделений с названиями
 */
export async function getFlagSubdivisions(db, parentFlagId) {
    try {
        // Получаем все подразделения для данного родительского флага
        const subdivisions = await new Promise((resolve, reject) => {
            const transaction = db.transaction(["flagSubdivisions"], "readonly");
            const store = transaction.objectStore("flagSubdivisions");
            const index = store.index("parent_flag_id");
            const request = index.getAll(IDBKeyRange.only(parentFlagId));

            request.onsuccess = function(event) {
                resolve(event.target.result);
            };

            request.onerror = function(event) {
                console.error(`Ошибка при получении подразделений для флага "${parentFlagId}":`, event.target.error);
                resolve([]); // Возвращаем пустой массив в случае ошибки
            };
        });

        if (subdivisions.length === 0) {
            return [];
        }

        // Получаем названия подразделений
        const subdivisionIds = subdivisions.map(sub => sub.id);
        const subdivisionNames = await new Promise((resolve, reject) => {
            const transaction = db.transaction(["flagSubdivisionNames"], "readonly");
            const store = transaction.objectStore("flagSubdivisionNames");
            const request = store.getAll(); // Получаем все и фильтруем в коде

            request.onsuccess = function(event) {
                const allNames = event.target.result;
                const matchedNames = allNames.filter(name => subdivisionIds.includes(name.id));
                resolve(matchedNames);
            };

            request.onerror = function(event) {
                console.error(`Ошибка при получении названий подразделений для флага "${parentFlagId}":`, event.target.error);
                resolve([]); // Возвращаем пустой массив в случае ошибки
            };
        });

        // Ассоциируем названия с подразделениями
        const subdivisionsWithNames = subdivisions.map(sub => {
            const nameObj = subdivisionNames.find(name => name.id === sub.id);
            return {
                ...sub,
                ...nameObj
            };
        });

        return subdivisionsWithNames;
    } catch (error) {
        console.error("Ошибка при получении подразделений флага:", error);
        return [];
    }
}

/**
 * Получает все флаги, соответствующие запросу поиска и фильтрам
 * @param {IDBDatabase} db - Открытая база данных
 * @param {string} searchQuery - Запрос поиска
 * @param {object} filters - Выбранные фильтры
 * @returns {Promise<Array>} - Массив флагов
 */
export async function getFilteredFlags(db, searchQuery, filters) {
    // Получаем все флаги
    const flags = await getAllRecords(db, "flags");
    const flagNames = await getAllRecords(db, "flagNames");

    // Создаём карты для быстрого доступа
    const flagNamesMap = flagNames.reduce((map, name) => {
        if (!map[name.flag_id]) {
            map[name.flag_id] = [];
        }
        map[name.flag_id].push(name);
        return map;
    }, {});

    // Дополняем карту оригинальными именами
    flags.forEach(flag => {
        if (flag.originalNames && Array.isArray(flag.originalNames)) {
            if (!flagNamesMap[flag.id]) {
                flagNamesMap[flag.id] = [];
            }
            flag.originalNames.forEach(originalName => {
                flagNamesMap[flag.id].push({
                    flag_id: flag.id,
                    language_id: originalName.language_id,
                    original_name: originalName.original_name,
                });
            });

            // Если у флага есть parent_flag_id, наследуем цвета от родителя
            if (flag.parent_flag_id) {
                const parentFlag = flags.find(f => f.id === flag.parent_flag_id);
                if (parentFlag && parentFlag.colors) {
                    flag.colors = parentFlag.colors;
                }
            }
        }
    });
    
    // Фильтруем флаги по запросу поиска и выбранным фильтрам
    let filteredFlags = flags.filter(flag => {
        // **Поиск**
        let matchesSearch = true;
        if (searchQuery) {
            const lowerSearch = searchQuery.toLowerCase();

            // Проверка по ID флага
            const flagMatches = flag.id.toLowerCase().includes(lowerSearch);

            // Проверка по названиям флага
            const flagNamesForFlag = flagNamesMap[flag.id] || [];
            const namesMatch = flagNamesForFlag.some(nameObj => {
                const shortName = (nameObj.short_name || "").toLowerCase();
                const longName = (nameObj.long_name || "").toLowerCase();
                const originalName = (nameObj.original_name || "").toLowerCase();
                return shortName.includes(lowerSearch) || longName.includes(lowerSearch) || originalName.includes(lowerSearch);
            });

            matchesSearch = flagMatches || namesMatch;
        }

        if (!matchesSearch) return false;

        // **Фильтрация по Цветам**
        if (filters.colors.size > 0) {
            const flagColors = new Set(flag.colors);
            const commonColors = [...filters.colors].some(color => flagColors.has(color));
            if (!commonColors) return false;
        }

        // **Фильтрация по Типам**
        if (filters.types.size > 0) {
            const flagTypes = new Set(flag.types);
            const commonTypes = [...filters.types].some(type => flagTypes.has(type));
            if (!commonTypes) return false;
        }

        // **Фильтрация по Языкам**
        if (filters.languages.size > 0) {
            const namesForFlag = flagNamesMap[flag.id] || [];
            const hasMatchingLanguage = namesForFlag.some(nameObj => filters.languages.has(nameObj.language_id));
            if (!hasMatchingLanguage) return false;
        }

        return true;
    });

    return {filteredFlags, flagNamesMap};
}

// ----- Начало файла: ./mvc-js\model.js -----
// model.js - Управление данными, взаимодействие с IndexedDB и хранение состояния фильтров и пагинации

import { initDB, loadData, getAllRecords, getFilteredFlags, getFlagSubdivisions } from './db.js';
import { selectedFilters, paginationState } from './state.js';

export class Model {
    constructor() {
        this.db = null;
        this.languagesMap = {};
        this.colorsMap = {};
        this.categoriesMap = {};
        this.subdivisionsCache = {};
    }

    async initialize() {
        this.db = await initDB();
        await loadData(this.db);
        await this.populateLanguagesMap();
        await this.populateColorsMap();
        await this.populateCategoriesMap();
    }

    async populateLanguagesMap() {
        const languages = await this.getAllRecords("languages");
        this.languagesMap = languages.reduce((map, language) => {
            map[language.id] = language.en_name || "Unknown Language";
            return map;
        }, {});
    }

    getLanguagesMap() {
        return this.languagesMap;
    }

    async populateColorsMap() {
        const colors = await this.getAllRecords("colors");
        this.colorsMap = colors.reduce((map, color) => {
            map[color.id] = color;
            return map;
        }, {});
    }

    getColorsMap() {
        return this.colorsMap;
    }

    async populateCategoriesMap() {
        const categories = await this.getAllRecords("categories");
        this.categoriesMap = categories.reduce((map, category) => {
            map[category.id] = category;
            return map;
        }, {});
    }

    getCategoriesMap() {
        return this.categoriesMap;
    }

    setCategoriesMap(categories) {
        this.categoriesMap = categories.reduce((map, category) => {
            map[category.id] = category;
            return map;
        }, {});
    }

    getSelectedFilters() {
        return selectedFilters;
    }

    getPaginationState() {
        return paginationState;
    }

    async fetchFilteredFlags(searchQuery) {
        return await getFilteredFlags(this.db, searchQuery, selectedFilters);
    }

    async getAllRecords(storeName) {
        return await getAllRecords(this.db, storeName);
    }
    
    async getFlagSubdivisions(flagId) {
        if (this.subdivisionsCache[flagId]) {
            return this.subdivisionsCache[flagId];
        }
        const subdivisions = await getFlagSubdivisions(this.db, flagId);
        this.subdivisionsCache[flagId] = subdivisions;
        return subdivisions;
    }
}

// ----- Начало файла: ./mvc-js\state.js -----
// state.js
export const selectedFilters = {
    colors: new Set(),
    types: new Set(),
    languages: new Set()
};

export const paginationState = {
    currentPage: 1,
    pageSize: 36,
    isLoading: false,
    hasMore: true,
    filteredFlags: [],
    flagNamesMap: {}
};

// ----- Начало файла: ./mvc-js\utils.js -----
// utils.js - Вспомогательные функции

import i18next from 'https://cdn.skypack.dev/i18next@21.6.16';

/**
 * Показывает ошибку на экране
 * @param {string} messageKey - Ключ перевода сообщения об ошибке
 */
export function showError(messageKey) {
    const output = document.getElementById("output");
    const errorDiv = document.createElement("div");
    errorDiv.className = "error-message";
    errorDiv.textContent = i18next.t(messageKey);

    // Добавляем стилизацию для ошибки
    errorDiv.style.color = "red";
    errorDiv.style.backgroundColor = "#ffe6e6";
    errorDiv.style.padding = "10px";
    errorDiv.style.border = "1px solid red";
    errorDiv.style.borderRadius = "5px";

    output.innerHTML = ""; // Очищаем предыдущий контент
    output.appendChild(errorDiv);
}

/**
 * Функция debounce, которая ограничивает частоту вызовов функции
 * @param {Function} func - Функция для обертки
 * @param {number} delay - Задержка в миллисекундах
 * @returns {Function}
 */
export function debounce(func, delay) {
    let timeout;
    return function (...args) {
        clearTimeout(timeout);
        timeout = setTimeout(() => func.apply(this, args), delay);
    };
}

/**
 * Возвращает переведённое имя для объекта с полями en_name, ru_name, zh_name и т.д.
 * @param {object} item - Объект с названиями на разных языках.
 * @returns {string} - Название на текущем языке.
 */
export function getTranslatedName(item) {
    const currentLang = i18next.language || 'en';
    return item[`${currentLang}_name`] || item.en_name || 'Unknown';
}

// ----- Начало файла: ./mvc-js\view.js -----
// view.js - Управление отображением данных пользователю, рендеринг интерфейса и обновление контента на основе переводов

import { getTranslatedName } from './utils.js';
import i18next from 'https://cdn.skypack.dev/i18next@21.6.16';

export class View {
    constructor() {
        this.output = document.getElementById("output");
        this.loadingSpinner = document.getElementById("loading-spinner");
        this.flagCardTemplate = document.getElementById('flag-card-template');
        this.categoriesMap = {}; // Инициализируем карту категорий
    }

    initializeCategoriesMap(categoriesMap) {
        this.categoriesMap = categoriesMap;
    }

    renderFlags(flags, flagNamesMap, page, pageSize, languagesMap, colorsMap, categoriesMap) {
        // Если это первая страница, очищаем вывод
        if (page === 1) {
            this.clearOutput();
        }

        if (page === 1 && flags.length === 0) {
            this.output.innerHTML = `<p>${i18next.t('noFlagsFound')}</p>`;
            return;
        }

        const fragment = document.createDocumentFragment();

        const startIndex = (page - 1) * pageSize;
        const endIndex = startIndex + pageSize;
        const flagsToDisplay = flags.slice(startIndex, endIndex);

        flagsToDisplay.forEach(flag => {
            const card = this.createFlagCard(flag, flagNamesMap, languagesMap, colorsMap, categoriesMap);
            fragment.appendChild(card);
        });

        this.output.appendChild(fragment);
        this.initLazyLoadImages();
    }

    clearOutput() {
        this.output.innerHTML = "";
    }

    showLoading() {
        if (this.loadingSpinner) {
            this.loadingSpinner.style.display = "block";
        }
    }

    hideLoading() {
        if (this.loadingSpinner) {
            this.loadingSpinner.style.display = "none";
        }
    }

    showError(messageKey) {
        const errorDiv = document.createElement("div");
        errorDiv.className = "error-message";
        errorDiv.textContent = i18next.t(messageKey);

        // Стилизация ошибки
        errorDiv.style.color = "red";
        errorDiv.style.backgroundColor = "#ffe6e6";
        errorDiv.style.padding = "10px";
        errorDiv.style.border = "1px solid red";
        errorDiv.style.borderRadius = "5px";

        this.output.innerHTML = "";
        this.output.appendChild(errorDiv);
    }

    createFlagCard(flag, flagNamesMap, languagesMap, colorsMap, categoriesMap) {
        const currentLang = i18next.language || 'en';
        const flagNames = flagNamesMap[flag.id] || [];
        const languagesMapRef = languagesMap;
        const colorsMapRef = colorsMap;
        const categoriesMapRef = categoriesMap;
    
        const card = document.createElement("div");
        card.className = "flag_card";
    
        // Индекс флага
        const flagIndex = document.createElement("span");
        flagIndex.className = "flag_index";
        flagIndex.textContent = flag.id;
        card.appendChild(flagIndex);
    
        // Изображение флага
        const flagImgDiv = document.createElement("div");
        flagImgDiv.className = "flag_img";
        const img = document.createElement("img");
        img.setAttribute("data-src", flag.parent_flag_id ? `flags/PNG-1200/PNG-0.5/${flag.parent_flag_id}-0.5-1200.png` : `flags/PNG-1200/PNG-0.5/${flag.id}-0.5-1200.png`);
        img.alt = flag.parent_flag_id ? `Флаг ${flag.parent_flag_id}` : `Флаг ${flag.id}`;
        img.classList.add("lazy-image");
        img.src = 'https://placehold.co/400'; // Заглушка до загрузки
    
        flagImgDiv.appendChild(img);
        card.appendChild(flagImgDiv);
    
        // Краткое название флага
        const shortName = document.createElement("div");
        shortName.className = "flag_shortname";
        const nameObj = flagNames.find(fn => fn.language_id === currentLang);
        shortName.textContent = nameObj ? nameObj.short_name : "";
        card.appendChild(shortName);
    
        // Полное название флага на текущем языке
        const longName = document.createElement("div");
        longName.className = "flag_longname";
        longName.textContent = nameObj?.long_name || "";
        card.appendChild(longName);
    
// Разделение имен на интерфейсные и дополнительные
const interfaceLanguages = i18next.options.supportedLngs || ['en'];

const additionalNames = flagNames.filter(nameObj => !interfaceLanguages.includes(nameObj.language_id));

// Отображение дополнительных имен за спойлером, если они есть
if (additionalNames.length > 0) {
    const details = document.createElement("details");
    details.className = "additional-names-details";

    const summary = document.createElement("summary");
    summary.textContent = i18next.t('showMoreNames'); // Переводится как "Показать больше имён"
    details.appendChild(summary);

    const namesList = document.createElement("ul");
    namesList.className = "additional-names-list";

    additionalNames.forEach(nameObj => {
        const li = document.createElement("li");
        const languageName = languagesMapRef[nameObj.language_id] || nameObj.language_id;
        li.textContent = `${languageName}: ${nameObj.original_name || nameObj.short_name || nameObj.long_name}`;
        namesList.appendChild(li);
    });

    details.appendChild(namesList);
    card.appendChild(details);
}

        // Проверка наличия subdivisions
        if (flag.subdivisions && flag.subdivisions.length > 0) {
            const details = document.createElement("details");
            details.className = "subdivisions-details";

            const summary = document.createElement("summary");
            summary.textContent = i18next.t('showSubdivisions'); // Переводится как "Показать подразделения"
            details.appendChild(summary);

            const subdivisionsTable = document.createElement("table");
            subdivisionsTable.className = "subdivisions-table";

            // Добавляем заголовки таблицы
            const headerRow = document.createElement("tr");
            headerRow.innerHTML = `
                <th>${i18next.t('category')}</th>
                <th>${i18next.t('name')}</th>
                <th>${i18next.t('originalName')}</th>
            `;
            subdivisionsTable.appendChild(headerRow);

            // Получение данных подразделений
            for (const subdivision of flag.subdivisions) {
                const row = document.createElement("tr");

                // Категория
                const categoryCell = document.createElement("td");
                const category = categoriesMapRef[subdivision.category_id];
                categoryCell.textContent = category ? getTranslatedName(category) : subdivision.category_id;
                row.appendChild(categoryCell);

                // Название на текущем языке
                const nameCell = document.createElement("td");
                // Предполагается, что подразделение содержит соответствующие поля названий
                // Например, nameObj может содержать original_name, short_name, long_name и т.д.
                // Для примера берём оригинальное имя на текущем языке
                const nameObj = subdivision.originalNames.find(name => name.language_id === i18next.language);
                nameCell.textContent = nameObj ? nameObj.original_name : "";
                row.appendChild(nameCell);

                // Оригинальное имя
                const originalNameCell = document.createElement("td");
                originalNameCell.textContent = nameObj ? nameObj.original_name : "";
                row.appendChild(originalNameCell);

                subdivisionsTable.appendChild(row);
            }

            details.appendChild(subdivisionsTable);
            card.appendChild(details);
        }

        // Цвета флага
        const colorsContainer = document.createElement("div");
        colorsContainer.className = "flag_colors_container";
    
        flag.colors.forEach(colorId => {
            const color = colorsMapRef[colorId];
            if (color) { // Добавлена проверка
                const colorDiv = document.createElement("div");
                colorDiv.className = "flag_color";
                colorDiv.title = getTranslatedName(color); // Используем функцию перевода
                colorDiv.style.backgroundColor = color.hex;
                colorsContainer.appendChild(colorDiv);
            } else {
                console.warn(`Цвет с ID ${colorId} не найден в colorsMap`);
            }
        });
    
        card.appendChild(colorsContainer);
    
        return card;
    }

    /**
     * Получает категорию по ID из текущей карты категорий
     * @param {string} categoryId
     * @returns {object|null}
     */
    getCategoryById(categoryId) {
        // Предполагается, что у модели есть метод для получения категорий
        // Вам необходимо добавить соответствующий метод в Model, например, getCategoriesMap
        // Затем хранить карту категорий внутри View или повторно запрашивать её
        console.log(this.categoriesMap);
        return this.categoriesMap[categoryId] || null;
    }

    initLazyLoadImages() {
        const lazyImages = document.querySelectorAll("img.lazy-image");

        if ("IntersectionObserver" in window) {
            const imageObserver = new IntersectionObserver((entries, observer) => {
                entries.forEach(entry => {
                    if (entry.isIntersecting) {
                        const img = entry.target;
                        const realSrc = img.getAttribute("data-src");
                        if (realSrc) {
                            img.src = realSrc;
                            img.onload = () => img.classList.add('loaded');
                            img.onerror = () => {
                                img.src = 'https://placehold.co/400/ff0000';
                                img.alt = 'Изображение не найдено';
                            };
                            img.removeAttribute("data-src");
                        }
                        imageObserver.unobserve(img);
                    }
                });
            }, {
                rootMargin: "200px 0px",
                threshold: 0.01
            });

            lazyImages.forEach(img => {
                imageObserver.observe(img);
            });
        } else {
            // Запасной вариант для браузеров без поддержки IntersectionObserver
            lazyImages.forEach(img => {
                const realSrc = img.getAttribute("data-src");
                if (realSrc) {
                    img.src = realSrc;
                    img.classList.add('loaded');
                    img.removeAttribute("data-src");
                }
            });
        }
    }

    updateContent() {
        // Перевод статических элементов
        document.querySelectorAll('[data-i18n]').forEach(function (element) {
            const key = element.getAttribute('data-i18n');
            if (key.includes('[placeholder]')) {
                const [, placeholderKey] = key.split(']');
                element.setAttribute('placeholder', i18next.t(placeholderKey));
            } else {
                element.textContent = i18next.t(key);
            }
        });
    }
}



// ----- Начало файла: ./mvc-js\data\categoryData.js -----
export const categoryData = [
	{ id: "1", en_name: "European collectivity", ru_name: "европейское территориальное образование", zh_name: "欧洲集体" },
	{ id: "2", en_name: "Land", ru_name: "земля", zh_name: "联邦州" },
	{ id: "3", en_name: "London borough", ru_name: "боро Лондона", zh_name: "伦敦自治市" },
];

// ----- Начало файла: ./mvc-js\data\colorData.js -----
export const colorData = [
	{ id: "1", ru_name: "пудровый", en_name: "powder", zh_name: "蜜桃色", hex: "#FFE2BD" },
	{ id: "2", ru_name: "розовый", en_name: "pink", zh_name: "浅珍珠红色", hex: "#ED72AA" },
	{ id: "3", ru_name: "фиолетовый", en_name: "violet", zh_name: "堇紫色", hex: "#742C64" },
];

// ----- Начало файла: ./mvc-js\data\flagData.js -----
export const flagData = [
	{
		id: "AB",
		is_index_ISO: false,
		resolution: "2:3",
		resolution_ratio: 0.67,
		colors: ["11", "15", "16", "21", "24", "7"],
		types: ["10", "2"],
		originalNames: [
			{
				language_id: "en",
				original_name: "Federal Republic of Ambazonia",
			},
		],
	},
	{
		id: "AM",
		is_index_ISO: true,
		resolution: "1:2",
		resolution_ratio: 0.5,
		colors: ["17", "18", "6"],
		types: ["11", "3"],
		originalNames: [
			{
				language_id: "hy",
				original_name: "Հայաստանի Հանրապետություն",
			},
		],
	},
	{
		id: "AQ-AT",
		is_index_ISO: false,
		resolution: "2:3",
		resolution_ratio: 0.67,
		colors: ["24", "5"],
		types: ["7"],
	},
	{
		id: "AQ-GB",
		is_index_ISO: false,
		resolution: "2:3",
		resolution_ratio: 0.67,
		colors: ["24", "7"],
		types: ["7"],
	},
	{
		id: "KZ",
		is_index_ISO: true,
		resolution: "1:2",
		resolution_ratio: 0.5,
		colors: ["16", "7"],
		types: ["11", "4"],
		originalNames: [
			{
				language_id: "kk",
				original_name: "Қазақстан Республикасы",
			},
			{
				language_id: "ru",
				original_name: "Республика Казахстан",
			},
		],
	},
];

// ----- Начало файла: ./mvc-js\data\flagNameData.js -----
export const flagNameData = [
	{ flag_id: "AB", language_id: "en", short_name: "Ambazonia", long_name: "Federal Republic of Ambazonia" },
	{ flag_id: "AB", language_id: "ru", short_name: "Амбазония", long_name: "Федеративная Республика Амбазония" },
	{ flag_id: "AB", language_id: "zh", short_name: "安巴佐尼亚", long_name: "安巴佐尼亚联邦共和国" },
	{ flag_id: "AC", language_id: "en", short_name: "Ascension Island", long_name: '' },
	{ flag_id: "AC", language_id: "ru", short_name: "Остров Вознесения", long_name: '' },
	{ flag_id: "AC", language_id: "zh", short_name: "阿森松岛", long_name: '' },
];

// ----- Начало файла: ./mvc-js\data\flagSubdivisionNamesData.js -----
export const flagSubdivisionNamesData = [
    {
        "id": "AD-02",
        "originalNames": [
            {
                "language_id": "ca",
                "original_name": "Canillo"
            }
        ],
        "en_name": "Canillo",
        "ru_name": "Канильо",
        "zh_name": "卡尼略"
    },
    {
        "id": "AF-BDG",
        "originalNames": [
            {
                "language_id": "prs",
                "original_name": "بادغیس"
            },
            {
                "language_id": "ps",
                "original_name": "بادغیس"
            }
        ],
        "en_name": "Badghis",
        "ru_name": "Бадгис",
        "zh_name": "巴德吉斯省"
    },
];

// ----- Начало файла: ./mvc-js\data\flagSubdivisionsData.js -----
export const flagSubdivisionsData = [
    {
        category_id: "150", 
        id: "AD-02",
        parent_flag_id: "AD",
    },
    {
        category_id: "150", 
        id: "AD-03",
        parent_flag_id: "AD",
    },
    {
        category_id: "150", 
        id: "AD-04",
        parent_flag_id: "AD",
    },
];

// ----- Начало файла: ./mvc-js\data\languageData.js -----
export const languageData = [
	{ id: "aa", ru_name: "афарский", en_name: "Afar", zh_name: "阿法尔语" },
	{ id: "ab", ru_name: "абхазский", en_name: "Abkhazian", zh_name: "阿布哈兹语" },
	{ id: "af", ru_name: "африкаанс", en_name: "Afrikaans", zh_name: "阿非利堪斯语" },
	{ id: "am", ru_name: "амхарский", en_name: "Amharic", zh_name: "阿姆哈拉语" },
];

// ----- Начало файла: ./mvc-js\data\typeData.js -----
export const typeData = [
	{ id: "1", ru_name: "Всего {count} флагов", en_name: "Total {count} flags", zh_name: "总共{count}个旗帜" },
	{ id: "2", ru_name: "Африка", en_name: "Africa", zh_name: "阿非利加洲" },
	{ id: "3", ru_name: "Европа", en_name: "Europe", zh_name: "欧洲" },
];


Файлы в папке data — лишь срез для примера, на самом деле данных намного больше. Проблемы, требующие решения, сейчас связаны с subdivisions и звучат так:

1) записи в полях имени и оригинального имени в таблице появляются только, если язык совпадает с текущим. Предполагалось, что в поле имени будет название subdivision на текущем языке (переводы есть в данных, ну ты видишь), а в оригинальном имени — уже на оригинальном языке. Причём есть случаи, когда оригинальных языков у subdivision несколько

2) перестала работать бесконечная прокрутка — дальше первой страницы флаги не загружаются, но и ошибок не выдают