(function (global) {
    'use strict';

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

    const DEFAULT_PREFERENCES = Object.freeze({
        fontFamily: 'Droid Serif, PT Serif, Georgia, serif',
        fontSize: 20,
        lineHeight: 1.58,
        margin: 28,
        textAlign: 'justify',
        theme: 'light',
        verticalScroll: false,
        pageTurnMode: 'tapSwipe',
        invertZones: false,
        brightnessGesture: false
    });

    const THEMES = Object.freeze({
        light: { background: '#FFFFFF', text: '#000000', muted: '#77716c' },
        sepia: { background: '#F6F3E0', text: '#000000', muted: '#8F806B' },
        dark: { background: '#000000', text: '#F4F4F4', muted: '#9D9C9F' }
    });

    const READER_FONT_STYLESHEET_URL = 'https://appassets.androidplatform.net/assets/reader_v2/fonts/fonts.css';
    const READER_FONT_STYLESHEET_ID = 'reader-v2-font-faces';
    const FONT_STACKS = Object.freeze({
        'Droid Serif': '"Droid Serif", "PT Serif", Georgia, serif',
        'EB Garamond': '"EB Garamond", "PT Serif", Georgia, serif',
        'Droid Sans': '"Droid Sans", Roboto, sans-serif',
        'Roboto': 'Roboto, "Open Sans", sans-serif',
        'PT Sans': '"PT Sans", Roboto, sans-serif',
        'PT Serif': '"PT Serif", Georgia, serif',
        'Merriweather': 'Merriweather, "PT Serif", Georgia, serif',
        'Open Sans': '"Open Sans", Roboto, sans-serif'
    });
    const BUNDLED_FONT_FAMILIES = Object.freeze(Object.keys(FONT_STACKS));

    function post(type, payload) {
        const envelope = JSON.stringify({
            version: 2,
            type: String(type || ''),
            payload: payload || {}
        });
        try {
            if (global.AndroidBridge && typeof global.AndroidBridge.postMessage === 'function') {
                global.AndroidBridge.postMessage(envelope);
            }
        } catch (_) {
            // A missing or detached native bridge must never break reading.
        }
        return envelope;
    }

    function errorPayload(error, stage, recoverable) {
        const value = error instanceof Error ? error : new Error(String(error || 'Неизвестная ошибка'));
        return {
            stage: stage || 'reader',
            code: value.code || value.name || 'ReaderError',
            message: value.message || String(value),
            recoverable: recoverable !== false
        };
    }

    function reportError(error, stage, recoverable) {
        const payload = errorPayload(error, stage, recoverable);
        post('error', payload);
        return payload;
    }

    function clamp(value, minimum, maximum) {
        const numeric = Number(value);
        if (!Number.isFinite(numeric)) return minimum;
        return Math.min(maximum, Math.max(minimum, numeric));
    }

    function normalizePreferences(input, base) {
        const source = input && typeof input === 'object' ? input : {};
        const previous = base && typeof base === 'object' ? base : DEFAULT_PREFERENCES;
        const theme = Object.prototype.hasOwnProperty.call(THEMES, source.theme)
            ? source.theme
            : (previous.theme || DEFAULT_PREFERENCES.theme);
        const textAlign = ['justify', 'left', 'right', 'center'].indexOf(source.textAlign) >= 0
            ? source.textAlign
            : (previous.textAlign || DEFAULT_PREFERENCES.textAlign);
        const pageTurnMode = ['tapSwipe', 'swipe', 'tap'].indexOf(source.pageTurnMode) >= 0
            ? source.pageTurnMode
            : (previous.pageTurnMode || DEFAULT_PREFERENCES.pageTurnMode);
        const requestedFontFamily = typeof source.fontFamily === 'string' && source.fontFamily.trim()
            ? source.fontFamily.trim()
            : previous.fontFamily;
        const bundledFontFamily = getBundledFontFamily(requestedFontFamily);
        const fontFamily = bundledFontFamily
            ? FONT_STACKS[bundledFontFamily]
            : requestedFontFamily;

        return {
            fontFamily: fontFamily,
            fontSize: clamp(source.fontSize === undefined ? previous.fontSize : source.fontSize, 12, 42),
            lineHeight: clamp(source.lineHeight === undefined ? previous.lineHeight : source.lineHeight, 1.1, 2.4),
            margin: clamp(source.margin === undefined ? previous.margin : source.margin, 8, 72),
            textAlign: textAlign,
            theme: theme,
            verticalScroll: source.verticalScroll === undefined
                ? Boolean(previous.verticalScroll)
                : Boolean(source.verticalScroll),
            pageTurnMode: pageTurnMode,
            invertZones: source.invertZones === undefined
                ? Boolean(previous.invertZones)
                : Boolean(source.invertZones),
            brightnessGesture: source.brightnessGesture === undefined
                ? Boolean(previous.brightnessGesture)
                : Boolean(source.brightnessGesture)
        };
    }

    function applyShellPreferences(preferences) {
        const root = document.documentElement;
        const theme = THEMES[preferences.theme] || THEMES.light;
        document.body.classList.remove('theme-light', 'theme-sepia', 'theme-dark');
        document.body.classList.add('theme-' + preferences.theme);
        root.style.setProperty('--reader-background', theme.background);
        root.style.setProperty('--reader-text', theme.text);
        root.style.setProperty('--reader-muted', theme.muted);
        root.style.setProperty('--reader-font-family', preferences.fontFamily);
        root.style.setProperty('--reader-font-size', preferences.fontSize + 'px');
        root.style.setProperty('--reader-line-height', String(preferences.lineHeight));
        root.style.setProperty('--reader-margin', preferences.margin + 'px');
        root.style.setProperty('--reader-text-align', preferences.textAlign);
    }

    function inferFormat(payload, bytes) {
        const format = String(payload && (payload.format || payload.mimeType || payload.mediaType) || '').toLowerCase();
        const url = String(payload && payload.url || '').toLowerCase().split(/[?#]/)[0];
        if (format.indexOf('fb2') >= 0 || /\.fb2$/.test(url)) return 'fb2';
        if (format.indexOf('epub') >= 0 || /\.epub$/.test(url)) return 'epub';
        if (bytes && bytes.length >= 4 && bytes[0] === 0x50 && bytes[1] === 0x4b) return 'epub';
        if (bytes) {
            const prefix = new TextDecoder('utf-8').decode(bytes.slice(0, Math.min(bytes.length, 768)));
            if (/<(?:\w+:)?FictionBook\b/i.test(prefix)) return 'fb2';
        }
        throw new Error('Не удалось определить формат книги. Поддерживаются EPUB и FB2.');
    }

    function flattenToc(items, depth, output) {
        const result = output || [];
        (items || []).forEach(function (item) {
            const label = String(item && item.label || '').replace(/\s+/g, ' ').trim();
            const href = String(item && item.href || '');
            if (label || href) {
                result.push({
                    label: label || href,
                    href: href,
                    depth: depth || 0
                });
            }
            flattenToc(item && (item.subitems || item.children), (depth || 0) + 1, result);
        });
        return result;
    }

    function makeExcerpt(text, matchIndex, queryLength) {
        const source = String(text || '').replace(/\s+/g, ' ').trim();
        if (!source) return '';
        const start = Math.max(0, matchIndex - 54);
        const end = Math.min(source.length, matchIndex + queryLength + 78);
        return (start > 0 ? '…' : '') + source.slice(start, end) + (end < source.length ? '…' : '');
    }

    function normalizeHref(href) {
        return String(href || '')
            .split('#')[0]
            .replace(/\\/g, '/')
            .replace(/^(\.\.\/)+/, '')
            .replace(/^\//, '');
    }

    function sameHref(left, right) {
        const a = normalizeHref(left);
        const b = normalizeHref(right);
        return Boolean(a && b && (a === b || a.endsWith('/' + b) || b.endsWith('/' + a)));
    }

    function nextFrame() {
        return new Promise(function (resolve) {
            requestAnimationFrame(function () {
                requestAnimationFrame(resolve);
            });
        });
    }

    function wait(milliseconds) {
        return new Promise(function (resolve) { setTimeout(resolve, milliseconds); });
    }

    function stableLocatorKey(locator) {
        if (typeof locator === 'string') return locator;
        const value = locator || {};
        return [
            value.type || '',
            value.cfi || '',
            value.sectionId || '',
            value.endSectionId || '',
            value.offset || 0,
            value.endOffset || value.offset || 0
        ].join('|');
    }

    function getBundledFontFamily(value) {
        const rawFamily = typeof value === 'string'
            ? value
            : String(value && value.fontFamily || '');
        const primaryFamily = rawFamily
            .split(',')[0]
            .trim()
            .replace(/^["']|["']$/g, '')
            .replace(/\s+/g, ' ')
            .toLowerCase();
        for (let index = 0; index < BUNDLED_FONT_FAMILIES.length; index += 1) {
            const family = BUNDLED_FONT_FAMILIES[index];
            if (family.toLowerCase() === primaryFamily) return family;
        }
        return null;
    }

    function isBundledReaderFont(value) {
        return Boolean(getBundledFontFamily(value));
    }

    function installReaderFontFaces(doc) {
        if (!doc || !doc.head) return Promise.resolve(false);
        let link = doc.getElementById(READER_FONT_STYLESHEET_ID);
        if (link && link.dataset.readerV2FontState === 'ready') return Promise.resolve(true);
        if (link && link.dataset.readerV2FontState === 'failed') return Promise.resolve(false);
        if (link) {
            try {
                if (link.sheet) {
                    link.dataset.readerV2FontState = 'ready';
                    return Promise.resolve(true);
                }
            } catch (_) {
                // A newly attached cross-origin sheet is confirmed by its load event below.
            }
            if (link.readerV2FontPromise) return link.readerV2FontPromise;
        } else {
            link = doc.createElement('link');
            link.id = READER_FONT_STYLESHEET_ID;
            link.rel = 'stylesheet';
            link.href = READER_FONT_STYLESHEET_URL;
        }

        link.readerV2FontPromise = new Promise(function (resolve) {
            let completed = false;
            let timeoutId = null;
            const finish = function (ready) {
                if (completed) return;
                completed = true;
                if (timeoutId !== null) global.clearTimeout(timeoutId);
                link.dataset.readerV2FontState = ready ? 'ready' : 'failed';
                resolve(ready);
            };
            link.addEventListener('load', function () { finish(true); }, { once: true });
            link.addEventListener('error', function () { finish(false); }, { once: true });
            timeoutId = global.setTimeout(function () {
                try { finish(Boolean(link.sheet)); }
                catch (_) { finish(false); }
            }, 8000);
            if (!link.parentNode) doc.head.appendChild(link);
        });
        return link.readerV2FontPromise;
    }

    function waitForPreferredFont(doc, preferences) {
        const family = getBundledFontFamily(preferences);
        if (!family || !doc) return Promise.resolve(true);
        return installReaderFontFaces(doc).then(function (stylesheetReady) {
            if (!stylesheetReady) return false;
            if (!doc.fonts || typeof doc.fonts.load !== 'function') return true;
            const size = clamp(preferences && preferences.fontSize || 20, 12, 42);
            const sample = 'АБВГДЕЁЖЗ абвгдеёжз 0123456789';
            const requests = ['normal 400 ' + size + 'px "' + family + '"'];
            if (family !== 'Droid Sans') {
                requests.push('italic 400 ' + size + 'px "' + family + '"');
            }
            return Promise.all(requests.map(function (request) {
                return doc.fonts.load(request, sample);
            })).then(function (results) {
                return results.every(function (faces) { return Boolean(faces && faces.length); });
            });
        }).catch(function () { return false; });
    }

    function validExternalUrl(rawUrl) {
        const value = String(rawUrl || '').trim();
        if (!/^(https?:|mailto:)/i.test(value)) return null;
        try {
            const url = new URL(value);
            return /^(https?:|mailto:)$/.test(url.protocol) ? url.href : null;
        } catch (_) {
            return null;
        }
    }

    function visibleSpeechText(documents, maxCharacters) {
        const limit = clamp(Number(maxCharacters) || 3500, 200, 3500);
        const blocks = [];
        const seen = new Set();
        (Array.isArray(documents) ? documents : [documents]).forEach(function (doc) {
            if (!doc || !doc.body) return;
            const view = doc.defaultView || global;
            const width = Math.max(1, view.innerWidth || doc.documentElement.clientWidth || 1);
            const height = Math.max(1, view.innerHeight || doc.documentElement.clientHeight || 1);
            const candidates = doc.body.querySelectorAll('p,h1,h2,h3,h4,h5,h6,pre,blockquote,li,td,th');
            Array.from(candidates).forEach(function (element) {
                if (element.querySelector('p,h1,h2,h3,h4,h5,h6,pre,blockquote,li,td,th')) return;
                const visible = Array.from(element.getClientRects()).some(function (rect) {
                    return rect.width > 0 && rect.height > 0 &&
                        rect.right > 0 && rect.bottom > 0 && rect.left < width && rect.top < height;
                });
                if (!visible) return;
                const text = String(element.innerText || element.textContent || '')
                    .replace(/\s+/g, ' ')
                    .trim();
                if (!text || seen.has(text)) return;
                seen.add(text);
                blocks.push(text);
            });
        });
        let text = blocks.join('\n').trim();
        if (text.length <= limit) return text;
        const candidate = text.slice(0, limit + 1);
        const sentenceEnd = Math.max(
            candidate.lastIndexOf('. '),
            candidate.lastIndexOf('! '),
            candidate.lastIndexOf('? '),
            candidate.lastIndexOf('… ')
        );
        const wordEnd = candidate.lastIndexOf(' ', limit);
        const cut = sentenceEnd >= Math.floor(limit * 0.55)
            ? sentenceEnd + 1
            : (wordEnd > 0 ? wordEnd : limit);
        return candidate.slice(0, cut).trim();
    }

    Internal.DEFAULT_PREFERENCES = DEFAULT_PREFERENCES;
    Internal.THEMES = THEMES;
    Internal.READER_FONT_STYLESHEET_URL = READER_FONT_STYLESHEET_URL;
    Internal.BUNDLED_FONT_FAMILIES = BUNDLED_FONT_FAMILIES;
    Internal.post = post;
    Internal.reportError = reportError;
    Internal.clamp = clamp;
    Internal.normalizePreferences = normalizePreferences;
    Internal.applyShellPreferences = applyShellPreferences;
    Internal.inferFormat = inferFormat;
    Internal.flattenToc = flattenToc;
    Internal.makeExcerpt = makeExcerpt;
    Internal.sameHref = sameHref;
    Internal.nextFrame = nextFrame;
    Internal.wait = wait;
    Internal.stableLocatorKey = stableLocatorKey;
    Internal.getBundledFontFamily = getBundledFontFamily;
    Internal.isBundledReaderFont = isBundledReaderFont;
    Internal.installReaderFontFaces = installReaderFontFaces;
    Internal.waitForPreferredFont = waitForPreferredFont;
    Internal.validExternalUrl = validExternalUrl;
    Internal.visibleSpeechText = visibleSpeechText;
})(window);
