import { DestroyRef, inject, Injectable, signal, Signal } from '@angular/core';
import { takeUntilDestroyed, toObservable, toSignal } from '@angular/core/rxjs-interop';
import { ActivatedRoute } from '@angular/router';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { environment } from '@environments/environment';
import { ErrorService, getConfig } from '@iqser/common-ui';
import { BASE_HREF_FN, shareDistinctLast } from '@iqser/common-ui/lib/utils';
import { TranslateService } from '@ngx-translate/core';
import WebViewer, { Core, WebViewerInstance, WebViewerOptions } from '@pdftron/webviewer';
import { AppConfig, File, IHeaderElement } from '@red/domain';
import { LicenseService } from '@services/license.service';
import { UserPreferenceService } from '@users/user-preference.service';
import { NGXLogger } from 'ngx-logger';
import { combineLatest, fromEvent, Observable } from 'rxjs';
import { map, startWith } from 'rxjs/operators';
import { DISABLED_HOTKEYS, DOCUMENT_LOADING_ERROR, USELESS_ELEMENTS } from '../utils/constants';
import { asList } from '../utils/functions';
import { Rgb } from '../utils/types';
import Annotation = Core.Annotations.Annotation;
import TextHighlightAnnotation = Core.Annotations.TextHighlightAnnotation;
import DocumentViewer = Core.DocumentViewer;
import Quad = Core.Math.Quad;
import TextTool = Core.Tools.TextTool;

@Injectable()
export class PdfViewer {
    readonly #convertPath = inject(BASE_HREF_FN);
    #instance: WebViewerInstance;
    readonly #licenseKey = inject(LicenseService).activeLicenseKey;
    readonly #config = getConfig<AppConfig>();
    readonly #isCompareMode = signal(false);
    readonly #isCompareMode$ = toObservable(this.#isCompareMode);
    readonly #searchButton: IHeaderElement = {
        type: 'actionButton',
        img: this.#convertPath('/assets/icons/general/pdftron-action-search.svg'),
        title: inject(TranslateService).instant(_('pdf-viewer.text-popup.actions.search')),
        onClick: () => {
            setTimeout(() => {
                this.#searchForSelectedText();
                this.#focusSearch();
            }, 250);
        },
    };
    readonly #destroyRef = inject(DestroyRef);
    readonly #logger = inject(NGXLogger);
    readonly #totalPages = signal<number>(0);
    readonly currentPage$ = inject(ActivatedRoute).queryParamMap.pipe(
        map(params => Number(params.get('page') ?? '1')),
        shareDistinctLast(),
    );
    readonly currentPage = toSignal(this.currentPage$);
    documentViewer: DocumentViewer;
    fileId: string;
    dossierId: string;
    pageChanged$: Observable<number>;
    readonly isCompareMode: Signal<boolean>;
    readonly totalPages: Signal<number>;

    searchOptions = {
        caseSensitive: true, // match case
        wholeWord: true, // match whole words only
        wildcard: false, // allow using '*' as a wildcard value
        regex: false, // string is treated as a regular expression
        searchUp: false, // search from the end of the document upwards
        ambientString: true, // return ambient string as part of the result
    };
    searchedWord?: string;

    constructor(
        private readonly _logger: NGXLogger,
        private readonly _errorService: ErrorService,
        private readonly _userPreferenceService: UserPreferenceService,
    ) {
        this.totalPages = this.#totalPages.asReadonly();
        this.isCompareMode = this.#isCompareMode.asReadonly();
    }

    get instance() {
        return this.#instance;
    }

    get PDFNet(): typeof Core.PDFNet {
        return this.#instance.Core.PDFNet;
    }

    get pageCount() {
        try {
            return this.documentViewer.getPageCount();
        } catch (e) {
            console.log(e);
            // might throw Error: getPageCount was called before the 'documentLoaded' event
            return 1;
        }
    }

    get #totalPages$() {
        const layoutChanged$ = fromEvent(this.documentViewer, 'layoutChanged').pipe(startWith(''));
        const docLoaded$ = fromEvent(this.documentViewer, 'documentLoaded');
        const docChanged$ = combineLatest([docLoaded$, layoutChanged$, this.#isCompareMode$]);

        return docChanged$.pipe(map(() => this.#adjustPage(this.pageCount)));
    }

    get #paginationOffset() {
        return this.isCompareMode() ? 2 : 1;
    }

    get #currentInternalPage() {
        return this.documentViewer.getCurrentPage();
    }

    get #pageChanged$() {
        const page$ = fromEvent<number>(this.documentViewer, 'pageNumberUpdated');
        return page$.pipe(map(page => this.#adjustPage(page)));
    }

    activateSearch() {
        this.#instance.UI.searchTextFull('', this.searchOptions);
    }

    deactivateSearch() {
        this.instance.UI.iframeWindow.document.getElementById('SearchPanel__input').blur();
        this.#updateSearchOptions();
        this.#instance.UI.closeElements(['searchPanel']);
    }

    resetAnnotationActions() {
        if (this.#instance.UI.annotationPopup.getItems().length) {
            this.#logger.info('[PDF] Reset annotation actions');
            this.#instance.UI.annotationPopup.update([]);
        }
    }

    navigateTo(page: string | number) {
        const parsedNumber = typeof page === 'string' ? parseInt(page, 10) : page;
        const paginationOffset = this.#paginationOffset;
        this.#navigateTo(paginationOffset === 2 ? parsedNumber * paginationOffset - 1 : parsedNumber);
    }

    navigatePreviousPage() {
        if (this.#currentInternalPage > 1) {
            this.#navigateTo(Math.max(this.#currentInternalPage - this.#paginationOffset, 1));
        }
    }

    navigateNextPage() {
        const pageCount = this.pageCount;
        if (this.#currentInternalPage < pageCount) {
            this.#navigateTo(Math.min(this.#currentInternalPage + this.#paginationOffset, pageCount));
        }
    }

    async init(htmlElement: HTMLElement) {
        this.#instance = await this.#getInstance(htmlElement);

        if (environment.production) {
            this.#instance.Core.setCustomFontURL('https://' + window.location.host + this.#convertPath('/assets/pdftron'));
        }

        await this.runWithCleanup(async () => {
            try {
                await this.PDFNet.initialize(this.#licenseKey);
            } catch (e) {
                this._errorService.set(e);
                throw e;
            }

            this.#instance.UI.setTheme(this._userPreferenceService.getTheme());
            this._logger.info('[PDF] Initialized');

            this.documentViewer = this.#instance.Core.documentViewer;

            this.pageChanged$ = this.#pageChanged$.pipe(shareDistinctLast());
            this.#totalPages$.pipe(takeUntilDestroyed(this.#destroyRef)).subscribe(pages => this.#totalPages.set(pages));
            this.#setSelectionMode();
            this.#configureElements();
            this.#disableHotkeys();
            this.#listenForCommandF();
            this.#listenForEsc();
            this.#clearSearchResultsWhenVisibilityChanged();
        });

        return this.#instance;
    }

    enable(dataElements: string[] | string) {
        this.#instance.UI.enableElements(asList(dataElements));
    }

    disable(dataElements: string[] | string) {
        this.#instance.UI.disableElements(asList(dataElements));
    }

    openCompareMode() {
        this._logger.info('[PDF] Open compare mode');
        this.#isCompareMode.set(true);
    }

    closeCompareMode() {
        this._logger.info('[PDF] Close compare mode');
        this.#isCompareMode.set(false);
    }

    runWithCleanup(action: () => Promise<void> | void) {
        return this.PDFNet.runWithCleanup(action, this.#licenseKey);
    }

    async loadDocument(blob: Blob, file: File, actionOnError?: () => void) {
        const onError = () => {
            this._errorService.set(DOCUMENT_LOADING_ERROR);
            this._logger.error('[PDF] Error while loading document');
            actionOnError?.();
        };

        this.fileId = file.fileId;
        this.dossierId = file.dossierId;

        this._logger.info('[PDF] Loading document...');

        await this.runWithCleanup(async () => {
            const document = await this.documentViewer.getDocument()?.getPDFDoc();
            await document?.lock();
            this.#instance.UI.loadDocument(blob, { documentId: file.fileId, filename: file?.filename ?? 'document.pdf', onError });
        });
    }

    quad(x1: number, y1: number, x2: number, y2: number, x3: number, y3: number, x4: number, y4: number) {
        return new this.#instance.Core.Math.Quad(x1, y1, x2, y2, x3, y3, x4, y4);
    }

    translateQuad(page: number, quad: Quad): Quad {
        const rotationsEnum = this.#instance.Core.PageRotation;
        switch (this.documentViewer.getCompleteRotation(page)) {
            case rotationsEnum.E_90:
                return this.quad(quad.x2, quad.y2, quad.x3, quad.y3, quad.x4, quad.y4, quad.x1, quad.y1);
            case rotationsEnum.E_180:
                return this.quad(quad.x3, quad.y3, quad.x4, quad.y4, quad.x1, quad.y1, quad.x2, quad.y2);
            case rotationsEnum.E_270:
                return this.quad(quad.x4, quad.y4, quad.x1, quad.y1, quad.x2, quad.y2, quad.x3, quad.y3);
            case rotationsEnum.E_0:
            default:
                return quad;
        }
    }

    color(rgb: Rgb) {
        return new this.#instance.Core.Annotations.Color(rgb.r, rgb.g, rgb.b);
    }

    rectangle() {
        return new this.#instance.Core.Annotations.RectangleAnnotation();
    }

    textHighlight() {
        return new this.#instance.Core.Annotations.TextHighlightAnnotation();
    }

    isTextHighlight(annotation: Annotation): annotation is TextHighlightAnnotation {
        return annotation instanceof this.#instance.Core.Annotations.TextHighlightAnnotation;
    }

    configureTextPopups(popups: IHeaderElement[]) {
        this.#instance.UI.textPopup.update([...popups, this.#searchButton, { dataElement: 'copyTextButton' }]);
    }

    #listenForCommandF() {
        this.#instance.UI.hotkeys.on('command+f, ctrl+f', e => {
            e.preventDefault();
            if (this.#isElementActive('searchPanel')) {
                this.#updateSearchOptions();
            } else {
                this.activateSearch();
            }
            if (this.documentViewer.getSelectedText()) {
                this.#searchForSelectedText();
            }
            setTimeout(() => this.#focusSearch(), 40);
        });
    }

    #listenForEsc() {
        this.#instance.UI.hotkeys.on('esc', e => {
            e.preventDefault();
            if (this.#isElementActive('searchPanel')) {
                this.#focusViewer();
                this.deactivateSearch();
            }
        });
    }

    #getSearchOption(optionId: string): boolean {
        const iframeWindow = this.#instance.UI.iframeWindow;
        const checkbox = iframeWindow.document.getElementById(optionId) as HTMLInputElement;
        return checkbox.checked;
    }

    #updateSearchOptions() {
        const wholeWord = this.#getSearchOption('whole-word-option');
        const caseSensitive = this.#getSearchOption('case-sensitive-option');
        this.searchOptions = { ...this.searchOptions, wholeWord: wholeWord, caseSensitive: caseSensitive };
    }

    #adjustPage(page: number) {
        if (this.isCompareMode()) {
            if (page % 2 === 1) {
                page = page + 1;
            }

            return page / 2;
        }

        return page;
    }

    #searchForSelectedText() {
        const uniqueText = [...new Set(this.documentViewer.getSelectedText().split('\n'))];
        const sameWord = [...new Set(uniqueText.map(text => text.toLowerCase()))].length === 1;
        if (uniqueText.length === 1 && sameWord) {
            this.searchedWord = uniqueText.join('\n');
        } else if (!sameWord) {
            this.searchedWord = this.documentViewer.getSelectedText();
        }
        const selected = this.searchedWord;
        this.#instance.UI.searchTextFull(selected, this.searchOptions);
    }

    #clearSearchResultsWhenVisibilityChanged() {
        const iframeWindow = this.#instance.UI.iframeWindow;
        iframeWindow.addEventListener('visibilityChanged', (event: any) => {
            if (event.detail.element !== 'searchPanel') {
                return;
            }

            const inputElement = iframeWindow.document.getElementById('SearchPanel__input') as HTMLInputElement;

            if (inputElement) {
                setTimeout(() => (inputElement.value = ''), 0);
            }

            if (!event.detail.isVisible) {
                this.documentViewer.clearSearchResults();
            }
        });
    }

    #navigateTo(pageNumber: number) {
        if (this.#currentInternalPage !== pageNumber) {
            this.documentViewer.displayPageLocation(pageNumber, 0, 0);
        }
    }

    #disableHotkeys(): void {
        DISABLED_HOTKEYS.forEach(key => this.#instance.UI.hotkeys.off(key));
    }

    #configureElements() {
        this.#instance.UI.disableElements(USELESS_ELEMENTS);
    }

    #setSelectionMode(): void {
        const textTool = this.#instance.Core.Tools.TextTool as unknown as TextTool;
        textTool.SELECTION_MODE = this.#config.SELECTION_MODE;
    }

    #getInstance(htmlElement: HTMLElement) {
        const options: WebViewerOptions = {
            licenseKey: this.#licenseKey,
            fullAPI: true,
            path: this.#convertPath('/assets/wv-resources'),
            css: this.#convertPath('/assets/pdftron/stylesheet.css'),
            backendType: 'ems',
        };

        return WebViewer(options, htmlElement);
    }

    #isElementActive(element: string): boolean {
        return this.#instance.UI.isElementOpen(element);
    }

    #focusSearch() {
        if (this.#isElementActive('textPopup')) {
            this.#instance.UI.closeElements(['textPopup']);
        }
        const iframeWindow = this.#instance.UI.iframeWindow;
        const input = iframeWindow.document.getElementById('SearchPanel__input') as HTMLInputElement;
        if (input) {
            input.focus();
        }
        if (input?.value.length > 0) {
            input.select();
        }
    }

    #focusViewer() {
        this.instance.UI.iframeWindow.document.getElementById('SearchPanel__input').blur();
        this.instance.UI.iframeWindow.focus();
    }
}
