(function (global) {
    'use strict';

    const Internal = global.ReaderV2Internal = global.ReaderV2Internal || {};

    class EpubEngine {
        constructor(mount, host) {
            this.mount = mount;
            this.host = host;
            this.book = null;
            this.rendition = null;
            this.toc = [];
            this.currentLocation = null;
            this.currentCfi = null;
            this.currentProgress = 0;
            this.ready = false;
            this.destroyed = false;
            this.searchState = { query: '', results: [], currentIndex: -1, generation: 0, searching: false };
            this.userHighlights = new Map();
            this.searchHighlightCfi = null;
            this.preparedDocuments = new WeakSet();
            this.resizeTimer = null;
            this.resizeObserver = null;
            this.pendingResizeTarget = undefined;
        }

        async load(arrayBuffer, payload) {
            if (typeof global.ePub !== 'function') throw new Error('Модуль EPUB недоступен.');
            if (typeof global.JSZip === 'undefined') throw new Error('Модуль ZIP недоступен.');

            this.book = global.ePub(arrayBuffer);
            await this.book.ready;
            if (this.destroyed) throw new Error('Загрузка EPUB отменена.');

            const navigation = await Promise.resolve(this.book.loaded && this.book.loaded.navigation)
                .catch(function () { return null; });
            const rawToc = navigation && navigation.toc
                ? navigation.toc
                : (this.book.navigation && this.book.navigation.toc || []);
            this.toc = Internal.flattenToc(rawToc, 0, []);
            Internal.post('toc', { format: 'epub', items: this.toc });

            const initialTarget = this._targetFromPayload(payload);
            let restoredLocator = false;
            try {
                await this._createRendition(initialTarget);
                restoredLocator = Boolean(initialTarget);
            } catch (error) {
                if (!initialTarget) throw error;
                console.warn('Не удалось восстановить сохранённую позицию EPUB; используется прогресс.', error);
                await this._createRendition(undefined);
            }
            await this._prepareLocations(payload && payload.cachedLocations);
            if (!restoredLocator && payload && Number.isFinite(Number(payload.progress))) {
                await this.goToProgress(payload.progress);
                await Internal.nextFrame();
            }
            if (this.destroyed) throw new Error('Загрузка EPUB отменена.');

            this.ready = true;
            this._observeResize();
            this._emitCurrentProgress();
            const metadata = await Promise.resolve(this.book.loaded && this.book.loaded.metadata)
                .catch(function () { return {}; });

            return {
                format: 'epub',
                title: metadata && metadata.title || payload.title || '',
                author: metadata && (metadata.creator || metadata.author) || payload.author || '',
                toc: this.toc,
                locator: this.currentLocator(),
                progress: this.currentProgress,
                totalPages: this._totalLocations()
            };
        }

        _targetFromPayload(payload) {
            const locator = payload && payload.locator;
            if (typeof locator === 'string' && locator.indexOf('epubcfi(') === 0) return locator;
            if (locator && typeof locator.cfi === 'string' && locator.cfi.indexOf('epubcfi(') === 0) {
                return locator.cfi;
            }
            return undefined;
        }

        async _createRendition(target) {
            const preferences = this.host.getPreferences();
            if (this.rendition) {
                try { this.rendition.destroy(); } catch (_) {}
            }
            this.mount.replaceChildren();
            this.rendition = this.book.renderTo(this.mount, {
                width: '100%',
                height: '100%',
                spread: 'none',
                manager: preferences.verticalScroll ? 'continuous' : 'default',
                flow: preferences.verticalScroll ? 'scrolled-doc' : 'paginated'
            });
            this._attachRenditionEvents();
            this._applyTheme(preferences);

            await this.rendition.display(target);
            await Internal.nextFrame();
            const contents = this.rendition.getContents ? this.rendition.getContents() : [];
            if (!contents || !contents.length) throw new Error('EPUB не создал отображаемую страницу.');
            contents.forEach(this._prepareContents.bind(this));
            await Promise.all(contents.map(function (item) {
                return Internal.waitForPreferredFont(item.document, preferences);
            }));
            this._restoreAnnotations();
        }

        _attachRenditionEvents() {
            const self = this;
            this.rendition.on('rendered', function (_section, view) {
                const contents = view && view.contents;
                if (contents) self._prepareContents(contents);
            });
            this.rendition.on('relocated', function (location) {
                self._onRelocated(location);
            });
            this.rendition.on('selected', function (cfiRange, contents) {
                let text = '';
                try { text = contents.window.getSelection().toString(); } catch (_) {}
                text = String(text || '').trim();
                if (!text) return;
                Internal.post('selection', {
                    format: 'epub',
                    text: text,
                    locator: { type: 'epub', cfi: cfiRange },
                    chapter: self._chapterForHref(self.currentLocation && self.currentLocation.start && self.currentLocation.start.href)
                });
            });
        }

        _prepareContents(contents) {
            if (!contents || !contents.document) return;
            Internal.installReaderFontFaces(contents.document);
            Internal.attachGestures(contents.document, {
                getPreferences: this.host.getPreferences,
                next: this.host.next,
                previous: this.host.previous,
                toggleControls: this.host.toggleControls,
                nativeTapZones: true
            });
            if (!this.preparedDocuments.has(contents.document)) {
                this.preparedDocuments.add(contents.document);
                contents.document.addEventListener('click', function (event) {
                    const anchor = event.target && event.target.closest && event.target.closest('a[href]');
                    if (!anchor) return;
                    const external = Internal.validExternalUrl(anchor.getAttribute('href'));
                    if (!external) return;
                    event.preventDefault();
                    event.stopPropagation();
                    Internal.post('externalLink', { url: external, format: 'epub' });
                }, false);
            }
            this._applyTheme(this.host.getPreferences());
        }

        _epubStyles(preferences) {
            const theme = Internal.THEMES[preferences.theme] || Internal.THEMES.light;
            const alignment = preferences.textAlign + ' !important';
            const lineHeight = preferences.lineHeight + ' !important';
            const margin = preferences.margin + 'px !important';
            return {
                'html': {
                    'background': theme.background + ' !important',
                    'color': theme.text + ' !important'
                },
                'body': {
                    'box-sizing': 'border-box !important',
                    'min-height': '100% !important',
                    'margin': '0 !important',
                    'padding-top': '18px !important',
                    'padding-right': margin,
                    'padding-bottom': '18px !important',
                    'padding-left': margin,
                    'background': theme.background + ' !important',
                    'color': theme.text + ' !important',
                    'font-family': preferences.fontFamily + ' !important',
                    'font-size': preferences.fontSize + 'px !important',
                    'font-weight': '400 !important',
                    'font-optical-sizing': 'auto !important',
                    'line-height': lineHeight,
                    'text-align': alignment,
                    'hyphens': 'auto !important',
                    'overflow-wrap': 'anywhere !important',
                    '-webkit-user-select': 'text !important',
                    'user-select': 'text !important',
                    '-webkit-touch-callout': 'default !important',
                    'touch-action': 'pan-y !important'
                },
                'p': {
                    'line-height': lineHeight,
                    'text-align': alignment,
                    'orphans': '2',
                    'widows': '2'
                },
                'h1, h2, h3, h4, h5, h6': {
                    'color': theme.text + ' !important',
                    'font-family': preferences.fontFamily + ' !important',
                    'line-height': '1.28 !important',
                    'text-align': 'center !important',
                    'break-after': 'avoid !important'
                },
                'img, svg': {
                    'max-width': '100% !important',
                    'max-height': '76vh !important',
                    'object-fit': 'contain !important'
                },
                'a': { 'color': '#e85b20 !important' },
                'pre, code': {
                    'white-space': 'pre-wrap !important',
                    'overflow-wrap': 'anywhere !important'
                }
            };
        }

        _applyTheme(preferences) {
            if (!this.rendition || !this.rendition.themes) return;
            const theme = Internal.THEMES[preferences.theme] || Internal.THEMES.light;
            try {
                this.rendition.themes.default(this._epubStyles(preferences));
                this.rendition.themes.font(preferences.fontFamily);
                this.rendition.themes.fontSize(preferences.fontSize + 'px');
            } catch (_) {}
            this.mount.style.backgroundColor = theme.background;
        }

        async applyPreferences(next, previous) {
            if (!this.rendition || !this.book) return;
            const target = this.currentCfi || undefined;
            if (Boolean(next.verticalScroll) !== Boolean(previous && previous.verticalScroll)) {
                await this._createRendition(target);
            } else {
                this._applyTheme(next);
                try {
                    this.rendition.flow(next.verticalScroll ? 'scrolled-doc' : 'paginated');
                    this.rendition.resize();
                    if (target) await this.rendition.display(target);
                } catch (error) {
                    Internal.reportError(error, 'epub.preferences', true);
                }
            }
            await Internal.nextFrame();
            const contents = this.rendition && this.rendition.getContents ? this.rendition.getContents() : [];
            await Promise.all((contents || []).map(function (item) {
                return Internal.waitForPreferredFont(item.document, next);
            }));
            this._emitCurrentProgress();
        }

        _observeResize() {
            if (!global.ResizeObserver || this.resizeObserver) return;
            const self = this;
            this.resizeObserver = new ResizeObserver(function () {
                if (self.pendingResizeTarget === undefined) {
                    self.pendingResizeTarget = self.currentCfi || null;
                }
                clearTimeout(self.resizeTimer);
                self.resizeTimer = setTimeout(async function () {
                    if (!self.rendition || self.destroyed) return;
                    const target = self.pendingResizeTarget;
                    self.pendingResizeTarget = undefined;
                    try {
                        self.rendition.resize();
                        if (target) await self.rendition.display(target);
                    } catch (error) {
                        Internal.reportError(error, 'epub.resize', true);
                    }
                }, 120);
            });
            this.resizeObserver.observe(this.mount);
        }

        async _prepareLocations(cachedLocations) {
            if (!this.book || !this.book.locations || typeof this.book.locations.generate !== 'function') {
                throw new Error('EPUB не поддерживает расчёт позиций.');
            }
            let cacheLoaded = false;
            if (typeof cachedLocations === 'string' && cachedLocations.trim()) {
                try {
                    this.book.locations.load(cachedLocations);
                    cacheLoaded = this._totalLocations() > 0;
                } catch (_) {
                    cacheLoaded = false;
                }
            }
            if (cacheLoaded) return;

            await this.book.locations.generate(900);
            if (this._totalLocations() <= 0) throw new Error('Не удалось рассчитать позиции EPUB.');
            const saved = this.book.locations.save();
            if (typeof saved === 'string' && saved) {
                Internal.post('paginationCache', { locations: saved });
            }
        }

        _totalLocations() {
            try { return Math.max(0, Number(this.book.locations.length()) || 0); } catch (_) { return 0; }
        }

        _chapterForHref(href) {
            const chapter = this.toc.find(function (item) { return Internal.sameHref(href, item.href); });
            return chapter ? chapter.label : '';
        }

        _onRelocated(location) {
            this.currentLocation = location || null;
            this.currentCfi = location && location.start && location.start.cfi || this.currentCfi;
            this._emitCurrentProgress();
        }

        _emitCurrentProgress() {
            const location = this.currentLocation || {};
            const start = location.start || {};
            const end = location.end || {};
            const total = this._totalLocations();
            let index = -1;
            let progress = Number(start.percentage);
            if (this.currentCfi && total > 0) {
                try {
                    index = this.book.locations.locationFromCfi(this.currentCfi);
                    progress = this.book.locations.percentageFromCfi(this.currentCfi);
                } catch (_) {}
            }
            if (!Number.isFinite(progress)) progress = this.currentProgress || 0;
            this.currentProgress = Internal.clamp(progress, 0, 1);
            const displayed = start.displayed || {};
            const chapterCurrentPage = Number(displayed.page) || null;
            const chapterTotalPages = Number(displayed.total) || null;
            Internal.post('progress', {
                format: 'epub',
                progress: this.currentProgress,
                locator: this.currentLocator(),
                href: start.href || '',
                chapter: this._chapterForHref(start.href),
                currentPage: index >= 0 ? index + 1 : null,
                totalPages: total || null,
                chapterPage: chapterCurrentPage,
                chapterTotal: chapterTotalPages,
                chapterCurrentPage: chapterCurrentPage,
                chapterTotalPages: chapterTotalPages,
                remainingInChapter: chapterTotalPages && chapterCurrentPage
                    ? Math.max(0, chapterTotalPages - chapterCurrentPage)
                    : null,
                atStart: Boolean(location.atStart || start.atStart),
                atEnd: Boolean(location.atEnd || end.atEnd)
            });
        }

        currentLocator() {
            return this.currentCfi ? { type: 'epub', cfi: this.currentCfi } : null;
        }

        async next() {
            return this._navigate('next');
        }

        async previous() {
            return this._navigate('previous');
        }

        async _navigate(direction) {
            if (!this.rendition) return false;
            const before = this.currentCfi;
            try {
                if (direction === 'next') await this.rendition.next();
                else await this.rendition.prev();
                await Internal.wait(45);
                const handled = before !== this.currentCfi;
                Internal.post('navigation', {
                    direction: direction,
                    handled: handled,
                    boundary: handled ? null : (direction === 'next' ? 'end' : 'start'),
                    locator: this.currentLocator()
                });
                return handled;
            } catch (error) {
                Internal.reportError(error, 'epub.navigation', true);
                return false;
            }
        }

        async goToProgress(value) {
            const progress = Internal.clamp(value, 0, 1);
            let target = null;
            if (this._totalLocations() > 0) {
                try { target = this.book.locations.cfiFromPercentage(progress); } catch (_) {}
            }
            if (!target) {
                const spine = this.book.spine && (this.book.spine.spineItems || this.book.spine.items) || [];
                const item = spine[Math.min(spine.length - 1, Math.floor(progress * spine.length))];
                target = item && item.href;
            }
            if (target) await this.rendition.display(target);
        }

        async goToLocator(locator) {
            const cfi = typeof locator === 'string' ? locator : locator && locator.cfi;
            if (!cfi) return false;
            await this.rendition.display(cfi);
            return true;
        }

        async goToChapter(href) {
            if (!href) return false;
            await this.rendition.display(href);
            return true;
        }

        async search(query) {
            this.clearSearch(false);
            const normalized = String(query || '').trim();
            const generation = ++this.searchState.generation;
            this.searchState.query = normalized;
            this.searchState.searching = Boolean(normalized);
            this._postSearch();
            if (!normalized) return [];

            const spine = this.book.spine && (this.book.spine.spineItems || this.book.spine.items) || [];
            const results = [];
            for (let index = 0; index < spine.length && results.length < 500; index += 1) {
                if (generation !== this.searchState.generation) return [];
                const section = spine[index];
                try {
                    if (typeof section.load === 'function') await section.load(this.book.load.bind(this.book));
                    const matches = typeof section.find === 'function' ? section.find(normalized) : [];
                    matches.forEach(function (match) {
                        if (results.length >= 500) return;
                        results.push({
                            locator: { type: 'epub', cfi: match.cfi },
                            cfi: match.cfi,
                            excerpt: match.excerpt || normalized,
                            href: section.href || '',
                            chapter: this._chapterForHref(section.href || '')
                        });
                    }, this);
                } catch (error) {
                    Internal.reportError(error, 'epub.search.section', true);
                } finally {
                    try { if (typeof section.unload === 'function') section.unload(); } catch (_) {}
                }
            }
            if (generation !== this.searchState.generation) return [];
            this.searchState.results = results;
            this.searchState.currentIndex = results.length ? 0 : -1;
            this.searchState.searching = false;
            if (results.length) await this._showSearchResult(0);
            else this._postSearch();
            return results;
        }

        async nextSearch() {
            return this._showSearchResult(this.searchState.currentIndex + 1);
        }

        async previousSearch() {
            return this._showSearchResult(this.searchState.currentIndex - 1);
        }

        async _showSearchResult(index) {
            const results = this.searchState.results;
            if (!results.length) {
                this._postSearch();
                return null;
            }
            const normalized = ((index % results.length) + results.length) % results.length;
            this.searchState.currentIndex = normalized;
            const result = results[normalized];
            this._removeSearchAnnotation();
            await this.rendition.display(result.cfi);
            try {
                this.rendition.annotations.highlight(
                    result.cfi,
                    { search: true },
                    null,
                    'reader-search-current',
                    { fill: '#ef6c2f', 'fill-opacity': '.42' }
                );
                this.searchHighlightCfi = result.cfi;
            } catch (_) {}
            this._postSearch();
            return result;
        }

        _postSearch() {
            const current = this.searchState.currentIndex >= 0
                ? this.searchState.results[this.searchState.currentIndex]
                : null;
            Internal.post('search', {
                query: this.searchState.query,
                total: this.searchState.results.length,
                currentIndex: this.searchState.currentIndex,
                searching: this.searchState.searching,
                truncated: this.searchState.results.length >= 500,
                result: current
            });
        }

        _removeSearchAnnotation() {
            if (!this.searchHighlightCfi || !this.rendition || !this.rendition.annotations) return;
            try { this.rendition.annotations.remove(this.searchHighlightCfi, 'highlight'); } catch (_) {}
            this.searchHighlightCfi = null;
        }

        clearSearch(emit) {
            this._removeSearchAnnotation();
            const generation = this.searchState.generation + 1;
            this.searchState = { query: '', results: [], currentIndex: -1, generation: generation, searching: false };
            if (emit !== false) this._postSearch();
        }

        clearSelection() {
            if (!this.rendition || !this.rendition.getContents) return;
            this.rendition.getContents().forEach(function (contents) {
                try { contents.window.getSelection().removeAllRanges(); } catch (_) {}
            });
        }

        addHighlight(locator, color) {
            const cfi = typeof locator === 'string' ? locator : locator && locator.cfi;
            if (!cfi || !this.rendition || !this.rendition.annotations) return false;
            const normalizedColor = typeof color === 'string' && color.trim() ? color.trim() : '#ffd84a';
            const key = Internal.stableLocatorKey({ type: 'epub', cfi: cfi });
            this.removeHighlight({ type: 'epub', cfi: cfi });
            try {
                this.rendition.annotations.highlight(
                    cfi,
                    { color: normalizedColor },
                    null,
                    'reader-user-highlight',
                    { fill: normalizedColor, 'fill-opacity': '.48' }
                );
                this.userHighlights.set(key, { locator: { type: 'epub', cfi: cfi }, color: normalizedColor });
                return true;
            } catch (error) {
                Internal.reportError(error, 'epub.highlight.add', true);
                return false;
            }
        }

        removeHighlight(locator) {
            const cfi = typeof locator === 'string' ? locator : locator && locator.cfi;
            if (!cfi) return false;
            const key = Internal.stableLocatorKey({ type: 'epub', cfi: cfi });
            try { this.rendition.annotations.remove(cfi, 'highlight'); } catch (_) {}
            return this.userHighlights.delete(key);
        }

        setHighlights(list) {
            if (this.rendition && this.rendition.annotations) {
                this.userHighlights.forEach(function (item) {
                    try { this.rendition.annotations.remove(item.locator.cfi, 'highlight'); } catch (_) {}
                }, this);
            }
            this.userHighlights.clear();
            (Array.isArray(list) ? list : []).forEach(function (item) {
                if (item) this.addHighlight(item.locator || item, item.color);
            }, this);
            return this.userHighlights.size;
        }

        speechPage(maxCharacters) {
            const contents = this.rendition && this.rendition.getContents
                ? this.rendition.getContents()
                : [];
            return {
                text: Internal.visibleSpeechText(contents.map(function (item) { return item.document; }), maxCharacters),
                locator: this.currentLocator(),
                canAdvance: this.currentProgress < .9999
            };
        }

        _restoreAnnotations() {
            const saved = Array.from(this.userHighlights.values());
            this.userHighlights.clear();
            saved.forEach(function (item) { this.addHighlight(item.locator, item.color); }, this);
        }

        state() {
            return {
                format: 'epub',
                ready: this.ready,
                locator: this.currentLocator(),
                progress: this.currentProgress,
                totalPages: this._totalLocations(),
                tocCount: this.toc.length,
                search: {
                    query: this.searchState.query,
                    total: this.searchState.results.length,
                    currentIndex: this.searchState.currentIndex,
                    searching: this.searchState.searching
                },
                highlightCount: this.userHighlights.size
            };
        }

        destroy() {
            this.destroyed = true;
            clearTimeout(this.resizeTimer);
            this.pendingResizeTarget = undefined;
            if (this.resizeObserver) this.resizeObserver.disconnect();
            this.resizeObserver = null;
            this.clearSearch(false);
            try { if (this.rendition) this.rendition.destroy(); } catch (_) {}
            try { if (this.book) this.book.destroy(); } catch (_) {}
            this.rendition = null;
            this.book = null;
            this.mount.replaceChildren();
        }
    }

    Internal.EpubEngine = EpubEngine;
})(window);
