import { PDFPageProxy } from 'pdfjs-dist';
import { BrowserPDF417Reader } from '@zxing/browser';

import * as pdfJSx from 'pdfjs-dist';
import { BarcodeFormat, DecodeHintType, Result } from '@zxing/library';

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Ä\u008dka 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Ä\u008dka 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 image2canvas = 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 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 = {
    billInfo: BillInfo,
    barcodeImage: string,
};

/**
 * 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 hints = new Map();
        hints.set(DecodeHintType.POSSIBLE_FORMATS, [ BarcodeFormat.PDF_417 ]);
        hints.set(DecodeHintType.PURE_BARCODE, false);
    
        const codeReader = new BrowserPDF417Reader(hints);

        const width = canvas.width;
        const height = canvas.height;

        // Canvas can contain multiple PDF417 codes, so we need to try to find them all
        // The issue is that `BrowserPDF417Reader` can only decode one code at a time
        // and it will throw an error if it finds more than one code.
        // To solve this, we will try splitting the canvas into different number of subsections
        // and decode each subsection separately. The best result will be the one with the most codes found.
        const splits = [5,4,3,2,1,0];

        let bestResult: Array<DecodeResult>|null = null;

        for(let splitIx = 0; splitIx < splits.length; splitIx++) {
            const split = splits[splitIx];

            const overlap = split === 0 ? 0 : Math.round(height / 50); // 50% overlap ensuring that we don't miss any codes that might be split between sections
            const sectionHeight = split === 0 ?  height : (Math.floor( Math.floor(height / split) + overlap));


            const canvasSections = Array.from({ length: split+1 }, (_, i) => {

                const sectionCanvas = document.createElement('canvas');
                sectionCanvas.width = width;
                sectionCanvas.height = sectionHeight;
                const sectionContext = sectionCanvas.getContext('2d');

                if (!sectionContext) {
                    throw new Error('Failed to get canvas context');
                }

                // Calculate the starting Y position for each section
                const startY = i===0 ? 0 : i * (sectionHeight) - overlap;

                // Draw the section of the original canvas onto the new section canvas
                sectionContext.drawImage(canvas, 0, startY, width, sectionHeight, 0, 0, width, sectionHeight);
                return sectionCanvas;
            });


            const codesFoundInSection: Array<DecodeResult> = [];

            // Try to decode each section
            for (const sectionCanvas of canvasSections) {
                try {
                    // give browser a chance to re-paint
                    // this is needed to avoid UI freezing when decoding large images
                    await yieldToBrowser('decodeFromCanvas');


                    const result = await codeReader.decodeFromCanvas(sectionCanvas);

                    if (result) {
                        codesFoundInSection.push({
                            billInfo: parseHubText(result.getText()),
                            barcodeImage: copyBarcodeImage(sectionCanvas, result)
                        });
                    }
                    
                } catch (error) {
                    // If no code was found in the current section an error will be thrown
                    // -> we can ignore it
                    
                } finally {
                }
            }

            await yieldToBrowser('after decodeFromCanvas');

            // IF in this iteration we found less codes than in the previous best result,
            // we can stop searching for more codes
            // This is because the number of codes found in each section will only decrease
            // as we increase the number of sections (split)
            if(bestResult && codesFoundInSection.length <= bestResult.length) {
                return(bestResult);
            }

            bestResult = codesFoundInSection;
        };

        return(bestResult);

    } catch(ex:any) {
        console.log(ex);
        return(null);
    }
}

/**
 * Copies bar code from the given canvas
 * 
*/
const copyBarcodeImage = (canvas:HTMLCanvasElement, decoderResult:Result):string => {

    // get coordinates of bar code
    const points = decoderResult.getResultPoints();

    // get outter coordinates of the bar code
    const codeLocation = points.reduce((acc, point) => {
        
        const x = point.getX();
        const y = point.getY();
        let result = {
            top: y < acc.top ? y: acc.top,
            left: x < acc.left ? x: acc.left,
            bottom: y > acc.bottom ? y: acc.bottom,
            right: x > acc.right ? x: acc.right
        };

        return({
            ...result,
            width: result.right - result.left,
            height: result.bottom - result.top,
        });
    }, {
        top: Number.MAX_SAFE_INTEGER,
        left: Number.MAX_SAFE_INTEGER,
        bottom: 0,
        right: 0,
        width: 0,
        height: 0
    });

    // copy section of the canvas containing bar code to another canvas
    const tempCanvas = document.createElement('canvas');
    const tempContext = tempCanvas.getContext('2d');

    tempCanvas.width = codeLocation.width;
    tempCanvas.height = codeLocation.height;

    // Draw the portion of the original canvas onto the temporary canvas
    // Assuming you want to copy a 100x100 pixels square starting from (50, 50) of the original canvas
    tempContext?.drawImage(canvas, codeLocation.left, codeLocation.top, codeLocation.width, codeLocation.height, 0, 0, codeLocation.width, codeLocation.height);

    // Convert the temporary canvas to a data URL
    const dataURL = tempCanvas.toDataURL();

    return(dataURL);
}

/** 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 image2canvas(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));
}