/*
 * Copyright 2021 Netsyms Technologies.
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 */

const OpenTimestamps = require('opentimestamps');
const nwglobal = require('nwglobal');
const {jsPDF} = window.jspdf;

const certificateVersion = 1;
const pdfPageScale = 3;
const pdfAssumedDPI = 72;
var pdfDoc = null;
var pageNumber = 0;
var pdfTitle = "";

function addPDF() {
    openFileDialog(function (path) {
        var filedata = getFileAsUint8Array(path);

        // Get filename
        // https://stackoverflow.com/a/424006
        pdfTitle = path.split('\\').pop().split('/').pop();

        pdfjsLib.getDocument(filedata).promise.then(function (pdfDoc_) {
            pdfDoc = pdfDoc_;

            pdfDoc.getMetadata().then(function (meta) {
                if (typeof meta.contentDispositionFilename == "string") {
                    pdfTitle = meta.contentDispositionFilename;
                }
            }).catch(function (err) {
                console.log('Error getting PDF metadata: ', err);
            });

            renderAllPages(pdfDoc);
            pdfZoom("fitheight");
            $(".enable-when-doc-open").removeClass("disabled");
            // Initial/first page rendering
            //renderPage(pageNum);
        });
    }, ".pdf");
}

function closePDF(showuserconfirm) {
    if (showuserconfirm && !confirm("Are you sure you want to close? All unsaved changes will be lost.")) {
        return;
    }
    disableGuideBox();
    pageNumber = 0;
    pdfDoc = null;
    $("#page-canvas-container .page-canvas").remove();
    $(".enable-when-doc-open").addClass("disabled");
}

function analyzeSignedPDF() {
    if ($("#page-canvas-container .page-canvas").length > 0 && !confirm("Opening a PDF to analyze will close the open document. Are you sure?")) {
        return;
    }
    closePDF(false);
    openFileDialog(function (path, html5file) {
        var analyze = function (pdf) {
            var splitindex = pdf.indexOf("-----BEGIN PGP MESSAGE-----");
            if (splitindex == -1) {
                showAlert("Selected file does not contain any recognized signature data.");
                return;
            }
            var pdfdata = pdf.slice(0, splitindex);
            var sigdata = pdf.slice(splitindex).toString();

            var verify = function (pdfhash) {
                loadKeyFromLocalStorage(function () {
                    verifyMessage(sigdata, function (msg, fprint) {
                        parseAndDisplaySignature(msg, pdfhash, true, fprint);
                    }, function (err) {
                        console.error(err);
                        console.log(sigdata);
                        var base64 = sigdata.split("\n\n", 2)[1].split("\n-----END PGP MESSAGE-----")[0];
                        base64 = base64.substring(0, base64.lastIndexOf("\n")).replaceAll("\n", "");
                        try {
                            var msg = window.atob(base64).split("START", 2)[1].split("END", 2)[0];
                            parseAndDisplaySignature(msg, pdfhash, false, null);
                        } catch (ex) {
                            console.error(ex);
                            showAlert("Error: could not parse signature data.");
                        }
                    });
                });

                if (typeof nw != 'undefined') {
                    pdfjsLib.getDocument(pdf).promise.then(function (pdfDoc_) {
                        pdfDoc = pdfDoc_;

                        renderAllPages(pdfDoc);
                        pdfZoom("fitheight");
                    });
                } else {
                    var fileReader = new FileReader();
                    fileReader.onload = function () {
                        pdfjsLib.getDocument(new Uint8Array(this.result)).promise.then(function (pdfDoc_) {
                            pdfDoc = pdfDoc_;

                            renderAllPages(pdfDoc);
                            pdfZoom("fitheight");
                        });
                    };
                    fileReader.readAsArrayBuffer(html5file);
                }
                $(".enable-when-doc-open").removeClass("disabled");
            };

            if (typeof nw != 'undefined') {
                verify(calculateSHA256HashOfString(pdfdata));
            } else {
                window.crypto.subtle.digest("SHA-256", (new TextEncoder()).encode(pdfdata))
                        .then(hash => {
                            window.hash = hash;
                            // here hash is an arrayBuffer,
                            // so we'll connvert it to its hex version
                            let result = '';
                            const view = new DataView(hash);
                            for (let i = 0; i < hash.byteLength; i += 4) {
                                result += ('00000000' + view.getUint32(i).toString(16)).slice(-8);
                            }
                            verify(result);
                        });
            }
        };
        if (typeof nw != 'undefined') {
            // running in NW.js so we have Node
            analyze(Buffer.from(getFileAsUint8Array(path).buffer));
        } else {
            // no Node :(
            var fileReader = new FileReader();
            fileReader.onload = function (e) {
                analyze(e.target.result);
            }
            fileReader.readAsBinaryString(html5file);
        }

    }, ".pdf");
}

function parseAndDisplaySignature(msg, pdfhash, verified, fingerprint) {
    var msgparts = {};
    // Decode message contents
    var msglines = msg.split("\n");
    for (var i = 0; i < msglines.length; i++) {
        if (msglines[i].includes(":")) {
            var parts = msglines[i].split(":", 2);
            msgparts[parts[0]] = parts[1];
        }
    }
    if (typeof msgparts["HASH"] == "string") {
        if (msgparts["HASH"] == pdfhash) {
            if (verified) {
                $("#verifyModalStatusMessage").html("<i class=\"fas fa-check-circle\"></i> File contents match signature. File has not been changed since notarization.");
                $("#verifyModalStatusMessage").removeClass();
                $("#verifyModalStatusMessage").addClass(["alert", "alert-success"]);
            } else {
                $("#verifyModalStatusMessage").html("<i class=\"fas fa-question-circle\"></i> File contents match signature; however, \
could not verify signature authenticity. It's possible the file was changed then re-signed by an unknown person. If you have the \
public key file for the notary that signed the file, <span class=\"btn btn-secondary text-dark btn-sm\"onclick=\"openPublicKeyFile()\">click here</span> to use it, \
then run the analyze tool again to prove if it was changed since notarization.");
                $("#verifyModalStatusMessage").removeClass();
                $("#verifyModalStatusMessage").addClass(["alert", "alert-warning"]);
            }
        } else {
            $("#verifyModalStatusMessage").html("<i class=\"fas fa-exclamation-circle\"></i> File contents do not match signature. Document has been modified since notarization.");
            $("#verifyModalStatusMessage").removeClass();
            $("#verifyModalStatusMessage").addClass(["alert", "alert-danger"]);
        }
    } else {
        $("#verifyModalStatusMessage").html("<i class=\"fas fa-exclamation-circle\"></i> No file hash found in document signature. Could not verify document integrity.");
        $("#verifyModalStatusMessage").removeClass();
        $("#verifyModalStatusMessage").addClass(["alert", "alert-danger"]);
    }
    // Add extra data to a list below the big message
    $("#verifyModalDetailedInfoList").html("");
    if (typeof msgparts["DATE"] == "string" && isNaN(msgparts["DATE"]) == false) {
        var datestr = formatTimestamp("F j, Y g:i a", msgparts["DATE"]);
        $("#verifyModalDetailedInfoList").append('<li class="list-group-item"><i class="far fa-calendar-alt fa-fw"></i> Notarization date/time: ' + datestr + '</li>');
    }
    if (typeof msgparts["NOTARY"] == "string") {
        $("#verifyModalDetailedInfoList").append('<li class="list-group-item"><i class="fas fa-user fa-fw"></i> Notary: ' + sanitizeHTMLString(msgparts["NOTARY"]) + '</li>');
    }
    if (typeof msgparts["STATE"] == "string") {
        $("#verifyModalDetailedInfoList").append('<li class="list-group-item"><i class="fas fa-map-marked-alt fa-fw"></i> State: ' + sanitizeHTMLString(msgparts["STATE"]).toUpperCase() + '</li>');
    }
    if (typeof msgparts["OTS"] == "string") {
        try {
            var bytearray = new nwglobal.Array();
            var bytestrarray = msgparts["OTS"].match(/.{1,3}/g);
            for (var i = 0; i < bytestrarray.length; i++) {
                bytearray.push(bytestrarray[i] * 1);
            }
            const fileHashArr = nwglobal.Uint8Array.from(Buffer.from(pdfhash, 'hex'));
            const detached = OpenTimestamps.DetachedTimestampFile.fromHash(new OpenTimestamps.Ops.OpSHA256(), fileHashArr);
            const detachedOts = OpenTimestamps.DetachedTimestampFile.deserialize(bytearray);
            let options = {
                ignoreBitcoinNode: true,
                timeout: 5000
            };
            OpenTimestamps.verify(detachedOts, detached, options).then(verifyResult => {
                if (typeof verifyResult != "undefined") {
                    if (typeof verifyResult.bitcoin != undefined) {
                        $("#verifyModalDetailedInfoList").append('<li class="list-group-item"><i class="fab fa-bitcoin fa-fw"></i> '
                                + 'Signing time witnessed by the Bitcoin network, proving it was signed within a few hours of '
                                + formatTimestamp("g A", verifyResult.bitcoin.timestamp) + ' on '
                                + formatTimestamp("F j, Y", verifyResult.bitcoin.timestamp) + '.'
                                + '</li>');
                    }
                    if (typeof verifyResult.litecoin != undefined) {
                        $("#verifyModalDetailedInfoList").append('<li class="list-group-item"><i class="fab fa-litecoin fa-fw"></i> '
                                + 'Signing time witnessed by the Litecoin network, proving it was signed within a few hours of '
                                + formatTimestamp("g A", verifyResult.litecoin.timestamp) + ' on '
                                + formatTimestamp("F j, Y", verifyResult.litecoin.timestamp) + '.'
                                + '</li>');
                    }
                }
            });
        } catch (ex) {
            console.error(ex);
        }
    }

    $("#verifyModalDetailedInfoList").append('<li class="list-group-item"><i class="far fa-file fa-fw"></i> Actual file hash: ' + pdfhash + '</li>');

    if (typeof msgparts["HASH"] == "string") {
        $("#verifyModalDetailedInfoList").append('<li class="list-group-item"><i class="far fa-file fa-fw"></i> Signed file hash: ' + sanitizeHTMLString(msgparts["HASH"]) + '</li>');
    }

    if (typeof fingerprint == "string") {
        var fingerprintstart = fingerprint.substr(0, fingerprint.length - 16);
        var fingerprintend = fingerprint.substr(-16);
        $("#verifyModalDetailedInfoList").append('<li class="list-group-item"><i class="fas fa-fingerprint fa-fw"></i> Signature fingerprint: ' + fingerprintstart + '<b>' + fingerprintend + '</b></li>');
    }
    new bootstrap.Modal(document.getElementById('verifyModal')).show();
}

function generatePDF(callback) {
    var canvases = $("#page-canvas-container .page-canvas");
    var statustextEl = $("#statustext");
    statustextEl.html("<i class='fas fa-spin fa-spinner'></i> Processing document...");
    const pdf = new jsPDF({
        unit: "in",
        compress: true
    });
    // creating a PDF creates a blank page that we don't want to use,
    // as we haven't done the calculations yet
    pdf.deletePage(1);

    // Render each page in order with a pause in between to keep UI responsive
    var processPage = function (i) {
        if (i < canvases.length) {
            statustextEl.html("<i class='fas fa-spin fa-spinner'></i> Processing page " + (i + 1) + " of " + canvases.length + "...");
            console.log("Processing " + (i + 1));
            var canvas = canvases[i];
            var widthpx = canvas.getContext("2d").canvas.width;
            var heightpx = canvas.getContext("2d").canvas.height;
            var pageWidthInches = widthpx / (pdfAssumedDPI * pdfPageScale);
            var pageHeightInches = heightpx / (pdfAssumedDPI * pdfPageScale);
            console.log(pageWidthInches + " x " + pageHeightInches);
            var pageFormat = [pageWidthInches, pageHeightInches];
            var pageOrientation = (pageWidthInches > pageHeightInches ? "landscape" : "portrait");
            pdf.addPage(pageFormat, pageOrientation);
            pdf.addImage(canvases[i].toDataURL(), 0, 0, pageWidthInches, pageHeightInches);
            i++;
            setTimeout(function () {
                processPage(i)
            }, 100);
        } else {
            statustextEl.html("");
            callback(pdf);
        }
    }
    processPage(0);
}

function getPDFAsByteArray(pdf) {
    return pdf.output("arraybuffer");
}

function makeAndSaveSignedPDF(pdf, savepath, callback) {
    var pdfbuffer = pdf.output("arraybuffer");
    const hashstr = calculateSHA256HashOfBuffer(pdfbuffer);

    var detached = OpenTimestamps.DetachedTimestampFile.fromHash(new OpenTimestamps.Ops.OpSHA256(), nwglobal.Uint8Array.from(Buffer.from(hashstr, 'hex')));
    var otsbytes = "";

    var sign = function () {
        var message = "START"
                + "\nV:" + certificateVersion
                + "\nHASH:" + hashstr
                + "\nDATE:" + time()
                + (otsbytes != "" ? "\nOTS:" + otsbytes : "")
                + "\nNOTARY:" + getStorage("notary_name")
                + "\nSTATE:" + getStorage("notary_state")
                + "\nEND\n";
        signMessage(message, keymgr, function (sig) {
            writeToFile(savepath, Buffer.from(pdfbuffer));
            appendToFile(savepath, sig);

            //writeToFile(savepath + ".notsigned.pdf", Buffer.from(pdfbuffer));
            writeToFile(savepath + ".sig", sig);
            callback({
                signature: sig,
                hash: hashstr
            });
        });
    };

    OpenTimestamps.stamp(detached).then(() => {
        var bytearray = detached.serializeToBytes();
        var bytestr = "";
        for (var i = 0; i < bytearray.length; i++) {
            bytestr += (bytearray[i] + "").padStart(3, "0");
        }
        otsbytes = bytestr;
        sign();
    }).catch(() => {
        sign();
    });
}

function savePDF() {
    disableGuideBox();
    var statustextEl = $("#statustext");
    loadKeyFromLocalStorage(function (message, ok) {
        if (ok) {
            openSaveFileDialog(function (path) {
                generatePDF(function (pdf) {
                    statustextEl.html("<i class='fas fa-spin fa-spinner'></i> Signing document...");
                    makeAndSaveSignedPDF(pdf, path, function (result) {
                        statustextEl.html("<i class='fas fa-check'></i> Signed and saved!");
                        showAlert("<i class='fas fa-check'></i> File signed and saved. SHA256 of file (excluding appended signature): " + result.hash);
                        setTimeout(function () {
                            statustextEl.html("");
                        }, 5000);
                    });
                });
            }, "signed.pdf", ".pdf");
        } else {
            statustextEl.html("");
            showAlert("Error: " + message);
        }
    });
}

function pdfZoom(str) {
    disableGuideBox();
    if ($("#page-canvas-container .page-canvas").length == 0) {
        setTimeout(function () {
            pdfZoom(str);
        }, 100);
        return;
    }
    var widthpx = $("#page-canvas-container .page-canvas").css("width").replace("px", "") * 1;
    var zoomstep = 100;
    switch (str) {
        case "out":
            $("#page-canvas-container .page-canvas").css("height", "auto");
            widthpx -= zoomstep;
            $("#page-canvas-container .page-canvas").css("width", widthpx + "px");
            break;
        case "in":
            $("#page-canvas-container .page-canvas").css("height", "auto");
            widthpx += zoomstep;
            $("#page-canvas-container .page-canvas").css("width", widthpx + "px");
            break;
        case "fitwidth":
            $("#page-canvas-container .page-canvas").css("width", "calc(100% - 1rem)");
            $("#page-canvas-container .page-canvas").css("height", "auto");
            break;
        case "fitheight":
            $("#page-canvas-container .page-canvas").css("height", "calc(100% - 1rem)");
            $("#page-canvas-container .page-canvas").css("width", "auto");
            break;
    }
}

function getNewCanvas(pagenumber) {
    var canvas = document.createElement('canvas');
    canvas.id = "pdf-canvas-page-" + pagenumber;
    canvas.className = "page-canvas";
    return canvas;
}

function addPage() {
    if ($("#page-canvas-container .page-canvas").length == 0) {
        showToast("Please open a document first.");
        return;
    }
    pageNumber++;
    var canvas = getNewCanvas(pageNumber);
    var prevPageCanvas = $("#page-canvas-container .page-canvas#pdf-canvas-page-" + (pageNumber - 1))[0];
    canvas.width = prevPageCanvas.getContext("2d").canvas.width;
    canvas.height = prevPageCanvas.getContext("2d").canvas.height;
    var ctx = canvas.getContext('2d');
    ctx.fillStyle = 'white';
    ctx.fillRect(0, 0, canvas.width, canvas.height);

    $("#page-canvas-container").append(canvas);
}

function renderAllPages() {
    var startingPageNumber = pageNumber;
    var thisDocPageCount = pdfDoc.numPages;
    for (var i = 1; i <= pdfDoc.numPages; i++) {
        pdfDoc.getPage(i).then(function (page) {
            var viewport = page.getViewport({scale: pdfPageScale});
            var canvas = getNewCanvas(page.pageNumber + startingPageNumber);
            canvas.height = viewport.height;
            canvas.width = viewport.width;
            $("#page-canvas-container").append(canvas);

            // Render PDF page into canvas context
            var renderContext = {
                canvasContext: canvas.getContext("2d"),
                viewport: viewport
            };

            page.render(renderContext);
        });
    }
    pageNumber = pageNumber + thisDocPageCount;
    //document.getElementById('page_count').textContent = pageNumber;
}