/*
 * Based on PDFSign v1.0.0
 * https://github.com/Communication-Systems-Group/pdfsign.js
 *
 * Copyright 2015, Thomas Bocek, University of Zurich
 *
 * Licensed under the MIT license:
 * http://www.opensource.org/licenses/MIT
 */

import { ObjectIdentifier, UTCTime, OctetString } from "asn1js";

import {
  ContentInfo,
  SignedData,
  Attribute,
  SignerInfo,
  IssuerAndSerialNumber,
  SignedAndUnsignedAttributes,
  EncapsulatedContentInfo,
  getCrypto
} from "pkijs";

import { parseCertificate, parsePrivateKey } from "./signingUtilities";

//keep this import to include the patched version of pdfjs library
import { PDFJS } from "../lib/pdfjs.parser.js";

function createXrefTable(xrefEntries) {
  xrefEntries = sortOnKeys(xrefEntries);
  var retVal = "xref\n";
  var last = -2;
  for (var i in xrefEntries) {
    i = parseInt(i);
    if (typeof xrefEntries[i].offset === "undefined") {
      continue;
    }
    retVal += calcFlow(i, last, xrefEntries);
    var offset = xrefEntries[i].offset;
    retVal +=
      pad10(offset) +
      " " +
      pad5(xrefEntries[i].gen) +
      " " +
      (xrefEntries[i].free ? "f" : "n") +
      " \n";
    last = i;
  }
  return retVal;
}

function calcFlow(i, last, xrefEntries) {
  if (last + 1 === i) {
    return "";
  }
  var count = 1;
  while (
    typeof xrefEntries[i + count] !== "undefined" &&
    typeof xrefEntries[i + count].offset !== "undefined"
  ) {
    count++;
  }
  return i + " " + count + "\n";
}

function createTrailer(topDict, startxref, sha256Hex, size, prev) {
  var retVal = "trailer <<\n";
  retVal += "  /Size " + size + "\n";
  var refRoot = topDict.getRaw("Root");
  if (typeof refRoot !== "undefined") {
    retVal += "  /Root " + refRoot.num + " " + refRoot.gen + " R\n";
  }
  var refInfo = topDict.getRaw("Info");
  if (typeof refInfo !== "undefined") {
    retVal += "  /Info " + refInfo.num + " " + refInfo.gen + " R\n";
  }
  retVal +=
    "  /ID [<" +
    sha256Hex.substring(0, 32) +
    "><" +
    sha256Hex.substring(32, 64) +
    ">]\n";
  if (typeof prev !== "undefined") {
    retVal += "  /Prev " + prev + "\n";
  }
  retVal += ">>\n";
  retVal += "startxref\n";
  retVal += startxref + "\n";
  retVal += "%%EOF\n";
  return retVal;
}

function createXrefTableAppend(xrefEntries) {
  xrefEntries = sortOnKeys(xrefEntries);

  var retVal = "xref\n";
  var last = -2;
  for (var i in xrefEntries) {
    i = parseInt(i);
    if (typeof xrefEntries[i].offset === "undefined") {
      continue;
    }
    retVal += calcFlow(i, last, xrefEntries);
    var offset = xrefEntries[i].offset;
    retVal +=
      pad10(offset) +
      " " +
      pad5(xrefEntries[i].gen) +
      " " +
      (xrefEntries[i].free ? "f" : "n") +
      " \n";
    last = i;
  }
  return retVal;
}

//http://stackoverflow.com/questions/10946880/sort-a-dictionary-or-whatever-key-value-data-structure-in-js-on-word-number-ke
function sortOnKeys(dict) {
  var sorted = [];
  for (var key in dict) {
    sorted[sorted.length] = key;
  }
  sorted.sort();

  var tempDict = {};
  for (var i = 0; i < sorted.length; i++) {
    tempDict[sorted[i]] = dict[sorted[i]];
  }

  return tempDict;
}

function removeFromArray(array, from, to) {
  var cutlen = to - from;
  var buf = new Uint8Array(array.length - cutlen);

  for (var i = 0; i < from; i++) {
    buf[i] = array[i];
  }
  for (var i = to, len = array.length; i < len; i++) {
    buf[i - cutlen] = array[i];
  }
  return buf;
}

function findXrefBlocks(xrefBlocks) {
  var num = xrefBlocks.length / 2;
  var retVal = [];
  for (var i = 0; i < num; i++) {
    retVal.push({ start: xrefBlocks[i], end: xrefBlocks[i + num] });
  }
  return retVal;
}

function convertUint8ArrayToBinaryString(u8Array) {
  var i,
    len = u8Array.length,
    b_str = "";
  for (i = 0; i < len; i++) {
    b_str += String.fromCharCode(u8Array[i]);
  }
  return b_str;
}

function arrayObjectIndexOf(array, start, end, orig) {
  for (var i = 0, len = array.length; i < len; i++) {
    if (
      array[i].start === start &&
      array[i].end === end &&
      array[i].orig === orig
    ) {
      return i;
    }
  }
  return -1;
}

function pad10(num) {
  var s = "000000000" + num;
  return s.substr(s.length - 10);
}

function pad5(num) {
  var s = "0000" + num;
  return s.substr(s.length - 5);
}

function pad2(num) {
  var s = "0" + num;
  return s.substr(s.length - 2);
}

function findRootEntry(xref) {
  var rootNr = xref.root.objId.substring(0, xref.root.objId.length - 1);
  return xref.entries[rootNr];
}

function findSuccessorEntry(xrefEntries, current) {
  //find it first
  var currentOffset = current.offset;
  var currentMin = Number.MAX_SAFE_INTEGER;
  var currentMinIndex = -1;
  for (var i in xrefEntries) {
    if (xrefEntries[i].offset > currentOffset) {
      if (xrefEntries[i].offset < currentMin) {
        currentMin = xrefEntries[i].offset;
        currentMinIndex = i;
      }
    }
  }
  if (currentMinIndex === -1) {
    return current;
  }
  return xrefEntries[currentMinIndex];
}

function updateArray(array, pos, str) {
  var upd = stringToUint8Array(str);
  for (var i = 0, len = upd.length; i < len; i++) {
    array[i + pos] = upd[i];
  }
  return array;
}

function copyToEnd(array, from, to) {
  var buf = new Uint8Array(array.length + (to - from));
  for (var i = 0, len = array.length; i < len; i++) {
    buf[i] = array[i];
  }

  for (var i = 0, len = to - from; i < len; i++) {
    buf[array.length + i] = array[from + i];
  }
  return buf;
}

function insertIntoArray(array, pos, str) {
  var ins = stringToUint8Array(str);
  var buf = new Uint8Array(array.length + ins.length);
  for (var i = 0; i < pos; i++) {
    buf[i] = array[i];
  }
  for (var i = 0; i < ins.length; i++) {
    buf[pos + i] = ins[i];
  }
  for (var i = pos; i < array.length; i++) {
    buf[ins.length + i] = array[i];
  }
  return buf;
}

function stringToUint8Array(str) {
  var buf = new Uint8Array(str.length);
  for (var i = 0, strLen = str.length; i < strLen; i++) {
    buf[i] = str.charCodeAt(i);
  }
  return buf;
}

function uint8ArrayToString(buf, from, to) {
  if (typeof from !== "undefined" && typeof to !== "undefined") {
    var s = "";
    for (var i = from; i < to; i++) {
      s = s + String.fromCharCode(buf[i]);
    }
    return s;
  }
  return String.fromCharCode.apply(null, buf);
}

function findFreeXrefNr(xrefEntries, used) {
  used = typeof used !== "undefined" ? used : [];
  var inc = used.length;

  for (var i = 1; i < xrefEntries.length; i++) {
    var index = used.indexOf(i);
    var entry = xrefEntries["" + i];
    if (index === -1 && (typeof entry === "undefined" || entry.free)) {
      return i;
    }
    if (index !== -1) {
      inc--;
    }
  }
  return xrefEntries.length + inc;
}

function find(uint8, needle, start, limit) {
  start = typeof start !== "undefined" ? start : 0;
  limit = typeof limit !== "undefined" ? limit : Number.MAX_SAFE_INTEGER;

  var search = stringToUint8Array(needle);
  var match = 0;

  for (var i = start; i < uint8.length && i < limit; i++) {
    if (uint8[i] === search[match]) {
      match++;
    } else {
      match = 0;
      if (uint8[i] === search[match]) {
        match++;
      }
    }

    if (match === search.length) {
      return i + 1 - match;
    }
  }
  return -1;
}

function findBackwards(uint8, needle, start, limit) {
  start = typeof start !== "undefined" ? start : uint8.length;
  limit = typeof limit !== "undefined" ? limit : Number.MAX_SAFE_INTEGER;

  var search = stringToUint8Array(needle);
  var match = search.length - 1;

  for (var i = start; i >= 0 && i < limit; i--) {
    if (uint8[i] === search[match]) {
      match--;
    } else {
      match = search.length - 1;
      if (uint8[i] === search[match]) {
        match--;
      }
    }

    if (match === 0) {
      return i - 1;
    }
  }
  return -1;
}

function strHex(s) {
  var a = "";
  for (var i = 0; i < s.length; i++) {
    a = a + pad2(s.charCodeAt(i).toString(16));
  }
  return a;
}

async function sha256(array) {
  const cryptoLib = getCrypto();
  const digestTmpBuf = await cryptoLib.digest({ name: "SHA-256" }, array);
  const digestTmpArray = new Uint8Array(digestTmpBuf);
  const digestTmpStr = uint8ArrayToString(digestTmpArray);
  const sha256Hex = strHex(digestTmpStr);
  return sha256Hex;
}

function isSigInRoot(pdf) {
  if (typeof pdf.acroForm === "undefined") {
    return false;
  }

  if (!pdf.acroForm) {
    return false;
  }

  return pdf.acroForm.get("SigFlags") === 3;
}

function updateXrefOffset(xref, offset, offsetDelta) {
  for (var i in xref.entries) {
    if (xref.entries[i].offset >= offset) {
      xref.entries[i].offset += offsetDelta;
    }
  }
  for (var i in xref.xrefBlocks) {
    if (xref.xrefBlocks[i] >= offset) {
      xref.xrefBlocks[i] += offsetDelta;
    }
  }
}

function updateXrefBlocks(xrefBlocks, offset, offsetDelta) {
  for (var i in xrefBlocks) {
    if (xrefBlocks[i].start >= offset) {
      xrefBlocks[i].start += offsetDelta;
    }
    if (xrefBlocks[i].end >= offset) {
      xrefBlocks[i].end += offsetDelta;
    }
  }
}

function updateOffset(pos, offset, offsetDelta) {
  if (pos >= offset) {
    return pos + offsetDelta;
  }
  return pos;
}

function round256(x) {
  return Math.ceil(x / 256) * 256 - 1;
}

/**
 * (D:YYYYMMDDHHmmSSOHH'mm)
 * e.g. (D:20151210164400+01'00')
 * where:
 * YYYY shall be the year
 * MM shall be the month (01–12)
 * DD shall be the day (01–31)
 * HH shall be the hour (00–23)
 * mm shall be the minute (00–59)
 * SS shall be the second (00–59)
 * O shall be the relationship of local time to Universal Time (UT), and shall be denoted by one of the characters PLUS SIGN (U+002B) (+), HYPHEN-MINUS (U+002D) (-), or LATIN CAPITAL LETTER Z (U+005A) (Z) (see below)
 * HH followed by APOSTROPHE (U+0027) (') shall be the absolute value of the offset from UT in hours (00–23)
 * mm shall be the absolute value of the offset from UT in minutes (00–59)
 */
function now(date) {
  //date = typeof date !== 'undefined' ? date : new Date();
  var yyyy = date.getFullYear().toString();
  var MM = pad2(date.getMonth() + 1);
  var dd = pad2(date.getDate());
  var hh = pad2(date.getHours());
  var mm = pad2(date.getMinutes());
  var ss = pad2(date.getSeconds());
  return yyyy + MM + dd + hh + mm + ss + createOffset(date);
}

function createOffset(date) {
  var sign = date.getTimezoneOffset() > 0 ? "-" : "+";
  var offset = Math.abs(date.getTimezoneOffset());
  var hours = pad2(Math.floor(offset / 60));
  var minutes = pad2(offset % 60);
  return sign + hours + "'" + minutes;
}

async function newSig(
  pdf,
  root,
  rootSuccessor,
  date,
  signingCert,
  certificateChain,
  privateKey
) {
  try {
    // {annotEntry} is the ref to the annot widget. If we enlarge the array, make sure all the offsets
    // after the modification will be updated -> xref table and startxref
    var annotEntry = findFreeXrefNr(pdf.xref.entries);
    // we'll store all the modifications we make, as we need to adjust the offset in the PDF
    var offsetForm =
      find(pdf.stream.bytes, "<<", root.offset, rootSuccessor.offset) + 2;
    //first we need to find the root element and add the following:
    //
    // /AcroForm<</Fields[{annotEntry} 0 R] /SigFlags 3>>
    //
    var appendAcroForm =
      "/AcroForm<</Fields[" + annotEntry + " 0 R] /SigFlags 3>>";
    //before we insert the acroform, we find the right place for annotentry

    //we need to add Annots [x y R] to the /Type /Page section. We can do that by searching /Contents[
    var pages = pdf.catalog.catDict.get("Pages");
    //get first page, we have hidden sig, so don't bother
    var ref = pages.get("Kids")[0];
    var xref = pdf.xref.fetch(ref);
    var offsetContentEnd = xref.get("#Contents_offset");
    //we now search backwards, this is safe as we don't expect user content here
    var offsetContent = findBackwards(
      pdf.stream.bytes,
      "/Contents",
      offsetContentEnd
    );
    var appendAnnots = "/Annots[" + annotEntry + " 0 R]\n ";

    //now insert string into stream
    var array = insertIntoArray(pdf.stream.bytes, offsetForm, appendAcroForm);
    //recalculate the offsets in the xref table, only update those that are affected
    updateXrefOffset(pdf.xref, offsetForm, appendAcroForm.length);
    offsetContent = updateOffset(
      offsetContent,
      offsetForm,
      appendAcroForm.length
    );

    var array = insertIntoArray(array, offsetContent, appendAnnots);
    updateXrefOffset(pdf.xref, offsetContent, appendAnnots.length);
    offsetContent = -1; //not needed anymore, don't update when offset changes

    //Then add to the next free object (annotEntry)
    //add right before the xref table or stream
    //if its a table, place element before the xref table
    //
    // sigEntry is the ref to the signature content. Next we need the signature object
    var sigEntry = findFreeXrefNr(pdf.xref.entries, [annotEntry]);

    //
    // {annotEntry} 0 obj
    // <</F 132/Type/Annot/Subtype/Widget/Rect[0 0 0 0]/FT/Sig/DR<<>>/T(signature)/V Y 0 R>>
    // endobj
    //
    var append =
      annotEntry +
      " 0 obj\n<</F 132/Type/Annot/Subtype/Widget/Rect[0 0 0 0]/FT/Sig/DR<<>>/T(signature" +
      annotEntry +
      ")/V " +
      sigEntry +
      " 0 R>>\nendobj\n\n";

    // we want the offset just before the last xref table or entry
    var blocks = findXrefBlocks(pdf.xref.xrefBlocks);
    var offsetAnnot = blocks[0].start;
    array = insertIntoArray(array, offsetAnnot, append);
    //no updateXrefOffset, as the next entry will be following

    //
    // {sigEntry} 0 obj
    // <</Contents <0481801e6d931d561563fb254e27c846e08325570847ed63d6f9e35 ... b2c8788a5>
    // /Type/Sig/SubFilter/adbe.pkcs7.detached/Location(Ghent)/M(D:20120928104114+02'00')
    // /ByteRange [A B C D]/Filter/Adobe.PPKLite/Reason(Test)/ContactInfo()>>
    // endobj
    //

    //the next entry goes below the above
    var offsetSig = offsetAnnot + append.length;

    // Both {annotEntry} and {sigEntry} objects need to be added to the last xref table. The byte range needs
    // to be adjusted. Since the signature will always be in a gap, use first an empty sig
    // to check the size, add ~25% size, then calculate the signature and place in the empty
    // space.
    var start = sigEntry + " 0 obj\n<</Contents <";
    var dummy = await sign_pki(
      signingCert,
      certificateChain,
      privateKey,
      stringToUint8Array("A"),
      date
    );
    //TODO: Adobe thinks its important to have the right size, no idea why this is the case
    var crypto = new Array(round256(dummy.length * 2)).join("0");
    var middle =
      ">\n/Type/Sig/SubFilter/adbe.pkcs7.detached/Location()/M(D:" +
      now(date) +
      "')\n/ByteRange ";
    var byteRange = "[0000000000 0000000000 0000000000 0000000000]";
    var end = "/Filter/Adobe.PPKLite/Reason()/ContactInfo()>>\nendobj\n\n";
    //all together
    var append2 = start + crypto + middle + byteRange + end;
    var offsetByteRange = start.length + crypto.length + middle.length;

    array = insertIntoArray(array, offsetSig, append2);
    updateXrefOffset(pdf.xref, offsetAnnot, append2.length + append.length);

    //find the xref tables, remove them and also the EOF, as we'll write a new table
    var xrefBlocks = findXrefBlocks(pdf.xref.xrefBlocks);

    for (var i in xrefBlocks) {
      var oldSize = array.length;
      array = removeFromArray(array, xrefBlocks[i].start, xrefBlocks[i].end);
      var length = array.length - oldSize;
      updateXrefOffset(pdf.xref, xrefBlocks[i].start, length);

      //check for %%EOF and remove it as well
      var offsetEOF = find(
        array,
        "%%EOF",
        xrefBlocks[i].start,
        xrefBlocks[i].start + 20
      );
      if (offsetEOF > 0) {
        var lengthEOF = "%%EOF".length;
        array = removeFromArray(array, offsetEOF, offsetEOF + lengthEOF);
        updateXrefOffset(pdf.xref, offsetEOF, -lengthEOF);
        updateXrefBlocks(xrefBlocks, offsetEOF, -lengthEOF);
        offsetAnnot = updateOffset(offsetAnnot, offsetEOF, -lengthEOF);
        offsetSig = updateOffset(offsetSig, offsetEOF, -lengthEOF);
      }
      updateXrefBlocks(xrefBlocks, xrefBlocks[i].start, length);
      offsetAnnot = updateOffset(offsetAnnot, xrefBlocks[i].start, length);
      offsetSig = updateOffset(offsetSig, xrefBlocks[i].start, length);
    }

    var sha256Hex = await sha256(array);

    //add the new entries to the xref
    pdf.xref.entries[annotEntry] = { offset: offsetAnnot, gen: 0, free: false };
    pdf.xref.entries[sigEntry] = { offset: offsetSig, gen: 0, free: false };

    var xrefTable = createXrefTable(pdf.xref.entries);
    //also empty entries count as in the PDF spec, page 720 (example)
    xrefTable += createTrailer(
      pdf.xref.topDict,
      array.length,
      sha256Hex,
      pdf.xref.entries.length
    );
    array = insertIntoArray(array, array.length, xrefTable);

    //since we consolidate, no prev! [adjust /Prev -> rawparsing + offset]
    var from1 = 0;
    var to1 = offsetSig + start.length;
    var from2 = to1 + crypto.length;
    var to2 = array.length - from2 - 1;
    var byteRange =
      "[" +
      pad10(from1) +
      " " +
      pad10(to1 - 1) +
      " " +
      pad10(from2 + 1) +
      " " +
      pad10(to2) +
      "]";
    array = updateArray(array, offsetSig + offsetByteRange, byteRange);
    var data = removeFromArray(array, to1 - 1, from2 + 1);
    var crypto2 = await sign_pki(
      signingCert,
      certificateChain,
      privateKey,
      data.buffer,
      date
    );
    array = updateArray(array, to1, crypto2);
    return array;
  } catch (err) {
    throw new Error("Error creating new signature in PDF: " + err);
  }
}

async function appendSig(
  pdf,
  root,
  rootSuccessor,
  date,
  signingCert,
  certificateChain,
  privateKey
) {
  try {
    //copy root and the entry with contents to the end
    var startRoot = pdf.stream.bytes.length + 1;

    var array = copyToEnd(
      pdf.stream.bytes,
      root.offset - 1,
      rootSuccessor.offset
    );

    //since we signed the first one, we know how the pdf has to look like:
    var offsetAcroForm = find(array, "/AcroForm<</Fields", startRoot);
    var endOffsetAcroForm = find(array, "]", offsetAcroForm);

    var annotEntry = findFreeXrefNr(pdf.xref.entries);
    var sigEntry = findFreeXrefNr(pdf.xref.entries, [annotEntry]);

    var appendAnnot = " " + annotEntry + " 0 R";
    array = insertIntoArray(array, endOffsetAcroForm, appendAnnot);

    //we need to add Annots [x y R] to the /Type /Page section. We can do that by searching /Annots
    var pages = pdf.catalog.catDict.get("Pages");
    //get first page, we have hidden sig, so don't bother
    var contentRef = pages.get("Kids")[0];
    var xref = pdf.xref.fetch(contentRef);
    var offsetAnnotEnd = xref.get("#Annots_offset");
    //we now search ], this is safe as we signed it previously
    var endOffsetAnnot = find(array, "]", offsetAnnotEnd);
    var xrefEntry = pdf.xref.getEntry(contentRef.num);
    var xrefEntrySuccosser = findSuccessorEntry(pdf.xref.entries, xrefEntry);
    var offsetAnnotRelative = endOffsetAnnot - xrefEntrySuccosser.offset;
    var startContent = array.length;
    array = copyToEnd(array, xrefEntry.offset, xrefEntrySuccosser.offset);
    array = insertIntoArray(
      array,
      array.length + offsetAnnotRelative,
      appendAnnot
    );

    var startAnnot = array.length;
    var append =
      annotEntry +
      " 0 obj\n<</F 132/Type/Annot/Subtype/Widget/Rect[0 0 0 0]/FT/Sig/DR<<>>/T(signature" +
      annotEntry +
      ")/V " +
      sigEntry +
      " 0 R>>\nendobj\n\n";
    array = insertIntoArray(array, startAnnot, append);

    var startSig = array.length;
    var start = sigEntry + " 0 obj\n<</Contents <";
    var dummy = await sign_pki(
      signingCert,
      certificateChain,
      privateKey,
      stringToUint8Array("A"),
      date
    );
    //TODO: Adobe thinks its important to have the right size, no idea why this is the case
    var crypto = new Array(round256(dummy.length * 2)).join("0");
    var middle =
      ">\n/Type/Sig/SubFilter/adbe.pkcs7.detached/Location()/M(D:" +
      now(date) +
      "')\n/ByteRange ";
    var byteRange = "[0000000000 0000000000 0000000000 0000000000]";
    var end = "/Filter/Adobe.PPKLite/Reason()/ContactInfo()>>\nendobj\n\n";
    //all together
    var append2 = start + crypto + middle + byteRange + end;
    array = insertIntoArray(array, startSig, append2);

    var sha256Hex = await sha256(array);

    var prev = pdf.xref.xrefBlocks[0];
    var startxref = array.length;
    var xrefEntries = [];
    xrefEntries[0] = { offset: 0, gen: 65535, free: true };
    xrefEntries[pdf.xref.topDict.getRaw("Root").num] = {
      offset: startRoot,
      gen: 0,
      free: false
    };
    xrefEntries[contentRef.num] = { offset: startContent, gen: 0, free: false };
    xrefEntries[annotEntry] = { offset: startAnnot, gen: 0, free: false };
    xrefEntries[sigEntry] = { offset: startSig, gen: 0, free: false };
    var xrefTable = createXrefTableAppend(xrefEntries);
    xrefTable += createTrailer(
      pdf.xref.topDict,
      startxref,
      sha256Hex,
      xrefEntries.length,
      prev
    );
    array = insertIntoArray(array, array.length, xrefTable);

    var from1 = 0;
    var to1 = startSig + start.length;
    var from2 = to1 + crypto.length;
    var to2 = array.length - from2 - 1;
    var byteRange =
      "[" +
      pad10(from1) +
      " " +
      pad10(to1 - 1) +
      " " +
      pad10(from2 + 1) +
      " " +
      pad10(to2) +
      "]";

    array = updateArray(array, from2 + middle.length, byteRange);
    //now sign from1-to1 / from2-to2 and update byterange

    var data = removeFromArray(array, to1 - 1, from2 + 1);
    var crypto2 = await sign_pki(
      signingCert,
      certificateChain,
      privateKey,
      data.buffer,
      date
    );
    array = updateArray(array, to1, crypto2);
    return array;
  } catch (err) {
    throw new Error("Error appending signature in PDF: " + err);
  }
}

function loadPdf(pdfArray) {
  try {
    var pdf = new pdfjsCoreDocument.PDFDocument(false, pdfArray, "");
    pdf.parseStartXRef();
    pdf.parse();
    return pdf;
  } catch (err) {
    throw new Error("Error parsing PDF: " + err);
  }
}

//data must be Uint8Array
async function sign_pki(signingCert, certificateChain, privateKey, data, date) {
  console.log("Calling sign_pki");

  const crypto = getCrypto();

  //date = typeof date !== 'undefined' ?  date : new Date();

  const hashAlg = "SHA-256";

  const digest = await crypto.digest({ name: hashAlg }, data);

  const signedAttr = [];

  signedAttr.push(
    new Attribute({
      type: "1.2.840.113549.1.9.3",
      values: [new ObjectIdentifier({ value: "1.2.840.113549.1.7.1" })]
    })
  ); // contentType

  signedAttr.push(
    new Attribute({
      type: "1.2.840.113549.1.9.4",
      values: [new OctetString({ valueHex: digest })]
    })
  ); // messageDigest

  signedAttr.push(
    new Attribute({
      type: "1.2.840.113549.1.9.5",
      values: [new UTCTime({ valueDate: date })]
    })
  ); // signingTime

  const cmsSignedSimpl = new SignedData({
    version: 1,
    encapContentInfo: new EncapsulatedContentInfo({
      eContentType: "1.2.840.113549.1.7.1" // "data" content type
    }),
    signerInfos: [
      new SignerInfo({
        version: 1,
        sid: new IssuerAndSerialNumber({
          issuer: signingCert.issuer,
          serialNumber: signingCert.serialNumber
        }),
        signedAttrs: new SignedAndUnsignedAttributes({
          type: 0,
          attributes: signedAttr
        })
      })
    ],
    certificates: certificateChain
  });

  const signatureBuffer = await cmsSignedSimpl.sign(
    privateKey,
    0,
    hashAlg,
    data.buffer
  );

  const cmsSignedSchema = cmsSignedSimpl.toSchema(true);

  const cmsContentSimp = new ContentInfo({
    contentType: "1.2.840.113549.1.7.2",
    content: cmsSignedSchema
  });

  const _cmsSignedSchema = cmsContentSimp.toSchema();

  //region Make length of some elements in "indefinite form"
  _cmsSignedSchema.lenBlock.isIndefiniteForm = true;

  const block1 = _cmsSignedSchema.valueBlock.value[1];
  block1.lenBlock.isIndefiniteForm = true;

  const block2 = block1.valueBlock.value[0];
  block2.lenBlock.isIndefiniteForm = true;

  //endregion

  let cmsSignedBuffer = _cmsSignedSchema.toBER(false);

  const cmsSignedArray = new Uint8Array(cmsSignedBuffer);
  const cmsSignedString = uint8ArrayToString(cmsSignedArray);
  const hex = strHex(cmsSignedString);
  return hex;
}

async function signPdfObjects(
  pdfRaw,
  signingCert,
  certificateChain,
  privateKey,
  date
) {
  console.log("Calling signPdfObjects");

  date = typeof date !== "undefined" && date !== null ? date : new Date();

  if (pdfRaw instanceof Buffer) {
    pdfRaw = new Uint8Array(pdfRaw);
  } else if (pdfRaw instanceof ArrayBuffer) {
    pdfRaw = new Uint8Array(pdfRaw);
  }
  console.log("Calling loadPdf");
  var pdf = loadPdf(pdfRaw);
  console.log("Calling findRootEntry");
  var root = findRootEntry(pdf.xref);
  console.log("Calling findSuccessorEntry");
  var rootSuccessor = findSuccessorEntry(pdf.xref.entries, root);
  console.log("Calling isSigInRoot");
  if (!isSigInRoot(pdf)) {
    console.log("Calling newSig");
    return await newSig(
      pdf,
      root,
      rootSuccessor,
      date,
      signingCert,
      certificateChain,
      privateKey
    );
  } else {
    console.log("Calling appendSig");
    return await appendSig(
      pdf,
      root,
      rootSuccessor,
      date,
      signingCert,
      certificateChain,
      privateKey
    );
  }
}

export async function signPdf(
  pdfRaw,
  signingCert,
  certificateChain,
  privateKey
) {
  console.log("Calling signPdf");

  const signingCertObj = parseCertificate(signingCert);
  const certificateChainObj = [];
  certificateChainObj[0] = parseCertificate(signingCert);
  for (let i = 0; i < certificateChain.length; i++) {
    certificateChainObj[i + 1] = parseCertificate(certificateChain[i]);
  }

  let privateKeyDecoded;
  try {
    console.log("Calling parsePrivateKey");
    privateKeyDecoded = await parsePrivateKey(privateKey);
  } catch (e) {
    console.log("Error decoding private key: ", e);
    throw new Error("Error decoding private key: " + e);
  }

  return await signPdfObjects(
    pdfRaw,
    signingCertObj,
    certificateChainObj,
    privateKeyDecoded
  );
}