(function (global) {
    'use strict';

    const Internal = global.ReaderV2Internal;
    const epubMount = document.getElementById('epub-viewer');
    const fb2Mount = document.getElementById('fb2-viewer');
    const status = document.getElementById('reader-status');
    const statusText = document.getElementById('reader-status-text');

    const state = {
        preferences: Internal.normalizePreferences({}, Internal.DEFAULT_PREFERENCES),
        engine: null,
        format: null,
        payload: null,
        loading: false,
        loadGeneration: 0,
        abortController: null,
        lastError: null
    };

    function parseObject(value, label) {
        if (value === undefined || value === null) return {};
        if (typeof value === 'string') {
            try { return JSON.parse(value); }
            catch (_) { throw new Error((label || 'Данные') + ': передан некорректный JSON.'); }
        }
        if (typeof value !== 'object') throw new Error((label || 'Данные') + ': ожидался объект.');
        return value;
    }

    function showStatus(text, isError) {
        statusText.textContent = String(text || '');
        status.classList.toggle('is-error', Boolean(isError));
        status.classList.remove('is-hidden');
    }

    function hideStatus() {
        status.classList.add('is-hidden');
        status.classList.remove('is-error');
    }

    function activateMount(format) {
        epubMount.classList.toggle('is-active', format === 'epub');
        fb2Mount.classList.toggle('is-active', format === 'fb2');
    }

    function destroyEngine() {
        if (!state.engine) return;
        try { state.engine.destroy(); } catch (_) {}
        state.engine = null;
        state.format = null;
    }

    function layoutPreferencesChanged(previous, next) {
        return [
            'fontFamily', 'fontSize', 'lineHeight', 'margin',
            'textAlign', 'theme', 'verticalScroll'
        ].some(function (key) { return previous[key] !== next[key]; });
    }

    function callEngine(method, args, fallback) {
        if (!state.engine || typeof state.engine[method] !== 'function') return Promise.resolve(fallback);
        try {
            return Promise.resolve(state.engine[method].apply(state.engine, args || [])).catch(function (error) {
                Internal.reportError(error, 'api.' + method, true);
                return fallback;
            });
        } catch (error) {
            Internal.reportError(error, 'api.' + method, true);
            return Promise.resolve(fallback);
        }
    }

    const host = {
        getPreferences: function () { return state.preferences; },
        next: function () { return ReaderV2.next(); },
        previous: function () { return ReaderV2.previous(); },
        toggleControls: function () { Internal.post('toggleControls', {}); }
    };

    async function loadBook(rawPayload) {
        const payload = parseObject(rawPayload, 'Книга');
        if (typeof payload.url !== 'string' || !payload.url.trim()) {
            throw new Error('Для загрузки книги требуется payload.url.');
        }
        const generation = ++state.loadGeneration;
        if (state.abortController) state.abortController.abort();
        state.abortController = typeof AbortController === 'function' ? new AbortController() : null;
        state.loading = true;
        state.lastError = null;
        showStatus('Загрузка книги…', false);

        if (payload.preferences) await setPreferences(payload.preferences);

        let nextEngine = null;
        try {
            const response = await fetch(payload.url, {
                method: 'GET',
                cache: 'no-store',
                credentials: 'same-origin',
                signal: state.abortController && state.abortController.signal
            });
            if (!response.ok && response.status !== 0) {
                const error = new Error('Не удалось получить книгу: HTTP ' + response.status + '.');
                error.code = 'BookFetchHttpError';
                throw error;
            }
            const arrayBuffer = await response.arrayBuffer();
            if (generation !== state.loadGeneration) return null;
            if (!arrayBuffer.byteLength) throw new Error('Получен пустой файл книги.');

            const format = Internal.inferFormat(payload, new Uint8Array(arrayBuffer, 0, Math.min(arrayBuffer.byteLength, 1024)));
            destroyEngine();
            activateMount(format);
            nextEngine = format === 'epub'
                ? new Internal.EpubEngine(epubMount, host)
                : new Internal.Fb2Engine(fb2Mount, host);
            state.engine = nextEngine;
            state.format = format;
            state.payload = {
                id: payload.id === undefined ? null : payload.id,
                title: payload.title || '',
                author: payload.author || '',
                url: payload.url
            };

            showStatus(format === 'epub' ? 'Подготовка EPUB…' : 'Подготовка FB2…', false);
            const result = await nextEngine.load(arrayBuffer, payload);
            if (generation !== state.loadGeneration || state.engine !== nextEngine) {
                nextEngine.destroy();
                if (state.engine === nextEngine) {
                    state.engine = null;
                    state.format = null;
                    activateMount(null);
                }
                return null;
            }
            if (Array.isArray(payload.highlights)) nextEngine.setHighlights(payload.highlights);
            state.loading = false;
            hideStatus();
            await Internal.nextFrame();
            Internal.post('bookReady', Object.assign({}, result, {
                id: state.payload.id,
                preferences: state.preferences
            }));
            return result;
        } catch (error) {
            if (error && error.name === 'AbortError') return null;
            if (generation !== state.loadGeneration) return null;
            state.loading = false;
            state.lastError = Internal.reportError(error, 'book.load', true);
            showStatus(state.lastError.message, true);
            if (nextEngine && state.engine === nextEngine) {
                destroyEngine();
                activateMount(null);
            }
            return null;
        }
    }

    async function setPreferences(rawPreferences) {
        const input = parseObject(rawPreferences, 'Настройки');
        const previous = state.preferences;
        const next = Internal.normalizePreferences(input, previous);
        state.preferences = next;
        Internal.applyShellPreferences(next);
        const preferredFontReady = await Internal.waitForPreferredFont(document, next);
        const preferredFontFamily = Internal.getBundledFontFamily(next);
        if (preferredFontFamily && !preferredFontReady) {
            state.preferences = previous;
            Internal.applyShellPreferences(previous);
            throw new Error('Не удалось загрузить встроенный шрифт «' + preferredFontFamily + '».');
        }
        if (
            state.engine &&
            typeof state.engine.applyPreferences === 'function' &&
            layoutPreferencesChanged(previous, next)
        ) {
            try { await state.engine.applyPreferences(next, previous); }
            catch (error) { Internal.reportError(error, 'preferences.apply', true); }
        }
        return next;
    }

    function getState() {
        return {
            version: 2,
            loading: state.loading,
            format: state.format,
            book: state.payload,
            preferences: state.preferences,
            error: state.lastError,
            reader: state.engine && typeof state.engine.state === 'function' ? state.engine.state() : null
        };
    }

    const ReaderV2 = {
        loadBook: function (payload) {
            return loadBook(payload).catch(function (error) {
                state.lastError = Internal.reportError(error, 'api.loadBook', true);
                showStatus(state.lastError.message, true);
                return null;
            });
        },
        setPreferences: function (preferences) {
            return setPreferences(preferences).catch(function (error) {
                Internal.reportError(error, 'api.setPreferences', true);
                return state.preferences;
            });
        },
        next: function () { return callEngine('next', [], false); },
        previous: function () { return callEngine('previous', [], false); },
        goToProgress: function (value) { return callEngine('goToProgress', [value], false); },
        goToLocator: function (locator) { return callEngine('goToLocator', [locator], false); },
        goToChapter: function (href) { return callEngine('goToChapter', [href], false); },
        getStateJson: function () {
            try { return JSON.stringify(getState()); }
            catch (error) {
                Internal.reportError(error, 'api.getStateJson', true);
                return JSON.stringify({ version: 2, error: 'StateSerializationError' });
            }
        },
        getSpeechPageJson: function (maxCharacters) {
            try {
                return JSON.stringify(callEngine('speechPage', [maxCharacters], {
                    text: '', locator: null, canAdvance: false
                }));
            } catch (error) {
                Internal.reportError(error, 'api.getSpeechPageJson', true);
                return JSON.stringify({ text: '', locator: null, canAdvance: false });
            }
        },
        search: function (query) { return callEngine('search', [String(query || '')], []); },
        nextSearch: function () { return callEngine('nextSearch', [], null); },
        previousSearch: function () { return callEngine('previousSearch', [], null); },
        clearSearch: function () { return callEngine('clearSearch', [], undefined); },
        clearSelection: function () { return callEngine('clearSelection', [], undefined); },
        addHighlight: function (locator, color) { return callEngine('addHighlight', [locator, color], false); },
        removeHighlight: function (locator) { return callEngine('removeHighlight', [locator], false); },
        setHighlights: function (list) { return callEngine('setHighlights', [Array.isArray(list) ? list : []], 0); }
    };

    global.ReaderV2 = Object.freeze(ReaderV2);
    Internal.applyShellPreferences(state.preferences);

    global.addEventListener('error', function (event) {
        if (!event || !event.error) return;
        Internal.reportError(event.error, 'javascript.runtime', true);
    });
    global.addEventListener('unhandledrejection', function (event) {
        if (!event) return;
        Internal.reportError(event.reason || 'Необработанная ошибка Promise.', 'javascript.promise', true);
    });
    global.addEventListener('beforeunload', function () {
        if (state.abortController) state.abortController.abort();
        destroyEngine();
    });

    hideStatus();
    Internal.post('shellReady', {
        formats: ['epub', 'fb2'],
        api: [
            'loadBook', 'setPreferences', 'next', 'previous', 'goToProgress', 'goToLocator',
            'goToChapter', 'getStateJson', 'getSpeechPageJson', 'search', 'nextSearch', 'previousSearch',
            'clearSearch', 'clearSelection', 'addHighlight', 'removeHighlight', 'setHighlights'
        ]
    });
})(window);
