import { PDFPageProxy } from 'pdfjs-dist';
import { readBarcodes, prepareZXingModule, type ReaderOptions } from 'zxing-wasm/reader';

// Configure WASM file location (similar to how pdf.worker.min.mjs is configured)
prepareZXingModule({
    overrides: {
        locateFile: (path, prefix) => {
            if (path.endsWith('.wasm')) {
                return window.location.origin + '/zxing_reader.wasm';
            }
            return prefix + path;
        }
    }
});

export type BillInfo = {
    header: string,
    currency: string,
    amount: number,
    payerName: string,
    payerAddress: string,
    payerTown: string,
    payeeName: string,
    payeeAddress: string,
    payeeTown: string,
    IBAN: string,
    model: string,
    reference: string,
    code: string,
    description: string,
};

/** Breaks current microtask execution and gives the UI thread a chance to do a re-paint */
const yieldToBrowser = (_label: string) => new Promise<boolean>((resolve) => {
    setTimeout(() => {
        resolve(true);
    }, 0);
});

/**
 * Decodes a PDF417 barcode
 * @param text 
 * @returns 
 * @description
 * Example text: "HRVHUB30\nEUR\n000000000012422\nDEREŽIĆ NIKOLA\nULICA DIVKA BUDAKA 17/17\n10000 ZAGREB\nGPZ-Opskrba d.o.o.\nRadnička cesta 1\n10000 Zagreb\nHR3623400091110343158\nHR05\n02964686-0307\nGASB\nAkontacijska rata za 01.2024.\n"
 *
 *    Decoded into:
 *        header: HRVHUB30
 *        currency:EUR
 *        amount:000000000012422
 *        payerName:DEREŽIĆ NIKOLA
 *        payerAddress:ULICA DIVKA BUDAKA 17/17
 *        payerTown:10000 ZAGREB
 *        payeeName:GPZ-Opskrba d.o.o.
 *        payeeAddress:Radnička cesta 1
 *        payeeTown:10000 Zagreb
 *        IBAN:HR3623400091110343158
 *        model:HR05
 *        reference:02964686-0307
 *        code:GASB
 *        description:Akontacijska rata za 01.2024.
 *
 */
const parseHubText = (text: string): BillInfo => {
    const [
        header,
        currency,
        amount,
        payerName,
        payerAddress,
        payerTown,
        payeeName,
        payeeAddress,
        payeeTown,
        IBAN,
        model,
        reference,
        code,
        description,
    ] = text.split('\n');

    return {
        header,
        currency,
        amount: parseInt(amount, 10),
        payerName,
        payerAddress,
        payerTown,
        payeeName,
        payeeAddress,
        payeeTown,
        IBAN,
        model,
        reference,
        code,
        description,
    };
}

/**
 * Render an image from the given file onto a canvas.
 * @param {File} imageFile - a file containing an image
 * @return {Promise<HTMLCanvasElement>} the canvas with the image rendered onto it
 */
const file2canvas = async function (imageFile: File): Promise<HTMLCanvasElement> {

    const reader = new FileReader();

    const canvas = await new Promise<HTMLCanvasElement>((resolve, reject) => {
        reader.onload = (progressEvent: ProgressEvent<FileReader>) => {
            const img = new Image();

            img.onload = () => {
                const canvas = document.createElement('canvas');
                const ctx = canvas.getContext('2d');
                canvas.width = img.width;
                canvas.height = img.height;

                if (!ctx) {
                    reject("Context is not set")
                    return;
                }

                ctx.drawImage(img, 0, 0);

                resolve(canvas);
            };

            const result = (progressEvent.target as FileReader).result;

            img.src = result as string;
        };

        reader.onerror = (e) => reject(e);
        reader.readAsDataURL(imageFile);
    });

    return (canvas);
}

/**
 * Render an image from onto a canvas.
 * @param {String} imageBase64 - base64 encoded image string
 * @return {Promise<HTMLCanvasElement>} the canvas with the image rendered onto it
 */
const image2canvas = async function (imageBase64: string): Promise<HTMLCanvasElement> {

    const canvas = await new Promise<HTMLCanvasElement>((resolve, reject) => {
        const img = new Image();

        img.onload = () => {
            const canvas = document.createElement('canvas');
            const ctx = canvas.getContext('2d');
            canvas.width = img.width;
            canvas.height = img.height;

            if (!ctx) {
                reject("Context is not set")
                return;
            }

            ctx.drawImage(img, 0, 0);

            resolve(canvas);
        };

        img.src = imageBase64;
    });

    return (canvas);
};

/**
 * Render the first page of a PDF document onto a new canvas.
 * @param {File} pdfFile - a file containing a PDF document
 * @return {Promise<HTMLCanvasElement>} the canvas with the first page of the PDF
 */
const pdf2canvas = async function (pdfFile: File): Promise<Array<HTMLCanvasElement>> {

    const reader = new FileReader();
    const data = await new Promise<Uint8Array>((resolve, reject) => {
        reader.onload = (e) => resolve(new Uint8Array((e.target as FileReader).result as ArrayBuffer));
        reader.onerror = (e) => reject(e);
        reader.readAsArrayBuffer(pdfFile);
    });

    const pdfJS = await import('pdfjs-dist');

    // worker file was manually copied to the `public` folder
    pdfJS.GlobalWorkerOptions.workerSrc = window.location.origin + '/pdf.worker.min.mjs';

    const pdf = await pdfJS.getDocument(data).promise;

    const canvases: Array<HTMLCanvasElement> = [];

    for (let i = 0; i < pdf.numPages; i++) {
        const page: PDFPageProxy = await pdf.getPage(i + 1);

        const scale = 4;
        const viewport = page.getViewport({ scale });

        const canvas = document.createElement('canvas');
        const context = canvas.getContext('2d');
        canvas.height = viewport.height;
        canvas.width = viewport.width;

        await page.render({ canvasContext: context as CanvasRenderingContext2D, viewport }).promise;

        canvases.push(canvas);
    }

    return (canvases);
}

export type DecodeResult = {
    hub3aText: string,
    billInfo: BillInfo,
};

/**
 * Convert canvas to ImageData for zxing-wasm
 * @param canvas - HTMLCanvasElement to convert
 * @returns ImageData object
 */
const canvasToImageData = (canvas: HTMLCanvasElement): ImageData => {
    const ctx = canvas.getContext('2d');
    if (!ctx) {
        throw new Error('Failed to get canvas context');
    }
    return ctx.getImageData(0, 0, canvas.width, canvas.height);
};

/**
 * Searches the given canvas for all PDF417 codes and decodes them.
 * @param {HTMLCanvasElement} canvas - the canvas to search for PDF417 codes
 * @return {Promise<Array<DecodeResult> | null>} - an array of decoded results
 * */
const decodeFromCanvas = async (canvas: HTMLCanvasElement): Promise<Array<DecodeResult> | null> => {
    try {
        const readerOptions: ReaderOptions = {
            tryHarder: true,
            formats: ['PDF417'],
            maxNumberOfSymbols: 10,
        };

        // give browser a chance to re-paint
        // this is needed to avoid UI freezing when decoding large images
        await yieldToBrowser('decodeFromCanvas');

        const imageData = canvasToImageData(canvas);
        const results = await readBarcodes(imageData, readerOptions);

        const codesFound: Array<DecodeResult> = results
            .filter(result => result.text)
            .map((result) => ({
                hub3aText: result.text,
                billInfo: parseHubText(result.text),
            }));

        return (codesFound);

    } catch (error) {
        console.log(error);
        return (null);
    }
}

/** Finds PDF417 code within a base64 encoded image and decodes it */
export const decodeFromImage = async (imageBase64: string): Promise<DecodeResult | null> => {
    const canvas = await image2canvas(imageBase64);

    const readerOptions: ReaderOptions = {
        tryHarder: true,
        formats: ['PDF417'],
        maxNumberOfSymbols: 1,
    };

    const imageData = canvasToImageData(canvas);
    const results = await readBarcodes(imageData, readerOptions);

    if (results.length === 0) {
        return null;
    }

    const hub3aText = results[0].text;

    return ({
        hub3aText,
        billInfo: parseHubText(hub3aText)
    });
}

/** Finds PDF417 code within a file and decodes it */
const decodeFromFile = async (file: File): Promise<DecodeResult[] | null> => {
    switch (file.type) {
        case 'image/png':
        case 'image/jpeg':
            return (await decodeFromCanvas(await file2canvas(file)));
        case 'application/pdf':
            const pageCanvas = await pdf2canvas(file);
            // go through each page of the PDF and decode the PDF417 codes
            // if there are multiple pages, we will decode each page separately
            // and return the results from all pages
            const results = (await Promise.all(pageCanvas.map(async (canvas) => {
                await yieldToBrowser('decodeFromCanvas');
                return await decodeFromCanvas(canvas);
            })))
                // remove null results (pages with no PDF417 codes)
                .filter((result) => result !== null)
                // flatten the array of arrays into a single array
                .flat() as DecodeResult[];

            return (results);
        default:
            console.error(file.name, 'is not a .pdf file.');
            return null;
    }
}

/**
 * Render the first page of a PDF document onto a new canvas.
 * @param {Event} event - The change event from an HTMLInputElement.
 * @return {Promise<HTMLCanvasElement | null>} The canvas with the first page of the PDF, or null if the document is not a PDF.
 */
export async function findDecodePdf417(event: React.ChangeEvent<HTMLInputElement>): Promise<Array<DecodeResult> | null> {
    const file = (event.target as HTMLInputElement).files?.[0];

    if (!file) {
        console.error('No file was selected.');
        return null;
    }

    return (await decodeFromFile(file));
}
