/* Copyright 2014 Mozilla Foundation
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import fs from "fs";
import { optimizeCMap } from "./optimize.mjs";
import { parseAdobeCMap } from "./parse.mjs";
import path from "path";

function compressCmap(srcPath, destPath, verify) {
  const content = fs.readFileSync(srcPath).toString();
  const inputData = parseAdobeCMap(content);
  optimizeCMap(inputData);

  let out = writeByte((inputData.type << 1) | inputData.wmode);
  if (inputData.comment) {
    out += writeByte(0xe0) + writeString(inputData.comment);
  }
  if (inputData.usecmap) {
    out += writeByte(0xe1) + writeString(inputData.usecmap);
  }
  let i = 0;
  while (i < inputData.body.length) {
    const item = inputData.body[i++],
      subitems = item.items;
    const first = item.items[0];
    const sequence = item.sequence === true;
    const flags = (item.type << 5) | (sequence ? 0x10 : 0);
    let nextStart, nextCode;
    switch (item.type) {
      case 0:
        out +=
          writeByte(flags | getHexSize(first.start)) +
          writeNumber(subitems.length);
        out += first.start + writeNumber(subHex(first.end, first.start));
        nextStart = incHex(first.end);
        for (let j = 1; j < subitems.length; j++) {
          out +=
            writeNumber(subHex(subitems[j].start, nextStart)) +
            writeNumber(subHex(subitems[j].end, subitems[j].start));
          nextStart = incHex(subitems[j].end);
        }
        break;
      case 1:
        out +=
          writeByte(flags | getHexSize(first.start)) +
          writeNumber(subitems.length);
        out +=
          first.start +
          writeNumber(subHex(first.end, first.start)) +
          writeNumber(first.code);
        nextStart = incHex(first.end);
        for (let j = 1; j < subitems.length; j++) {
          out +=
            writeNumber(subHex(subitems[j].start, nextStart)) +
            writeNumber(subHex(subitems[j].end, subitems[j].start)) +
            writeNumber(subitems[j].code);
          nextStart = incHex(subitems[j].end);
        }
        break;
      case 2:
        out +=
          writeByte(flags | getHexSize(first.char)) +
          writeNumber(subitems.length);
        out += first.char + writeNumber(first.code);
        nextStart = incHex(first.char);
        nextCode = first.code + 1;
        for (let j = 1; j < subitems.length; j++) {
          out +=
            (sequence ? "" : writeNumber(subHex(subitems[j].char, nextStart))) +
            writeSigned(subitems[j].code - nextCode);
          nextStart = incHex(subitems[j].char);
          nextCode = item.items[j].code + 1;
        }
        break;
      case 3:
        out +=
          writeByte(flags | getHexSize(first.start)) +
          writeNumber(subitems.length);
        out +=
          first.start +
          writeNumber(subHex(first.end, first.start)) +
          writeNumber(first.code);
        nextStart = incHex(first.end);
        for (let j = 1; j < subitems.length; j++) {
          out +=
            (sequence
              ? ""
              : writeNumber(subHex(subitems[j].start, nextStart))) +
            writeNumber(subHex(subitems[j].end, subitems[j].start)) +
            writeNumber(subitems[j].code);
          nextStart = incHex(subitems[j].end);
        }
        break;
      case 4:
        out +=
          writeByte(flags | getHexSize(first.code)) +
          writeNumber(subitems.length);
        out += first.char + first.code;
        nextStart = incHex(first.char);
        nextCode = incHex(first.code);
        for (let j = 1; j < subitems.length; j++) {
          out +=
            (sequence ? "" : writeNumber(subHex(subitems[j].char, nextStart))) +
            writeSigned(subHex(subitems[j].code, nextCode));
          nextStart = incHex(subitems[j].char);
          nextCode = incHex(subitems[j].code);
        }
        break;
      case 5:
        out +=
          writeByte(flags | getHexSize(first.code)) +
          writeNumber(subitems.length);
        out +=
          first.start +
          writeNumber(subHex(first.end, first.start)) +
          first.code;
        nextStart = incHex(first.end);
        for (let j = 1; j < subitems.length; j++) {
          out +=
            (sequence
              ? ""
              : writeNumber(subHex(subitems[j].start, nextStart))) +
            writeNumber(subHex(subitems[j].end, subitems[j].start)) +
            subitems[j].code;
          nextStart = incHex(subitems[j].end);
        }
        break;
    }
  }

  fs.writeFileSync(destPath, Buffer.from(out, "hex"));

  if (verify) {
    const result2 = parseCMap(out);
    const isGood = JSON.stringify(inputData) === JSON.stringify(result2);
    if (!isGood) {
      throw new Error("Extracted data does not match the expected result");
    }
  }

  return {
    orig: fs.statSync(srcPath).size,
    packed: out.length >> 1,
  };
}

function parseCMap(binaryData) {
  const reader = {
    buffer: binaryData,
    pos: 0,
    end: binaryData.length,
    readByte() {
      if (this.pos >= this.end) {
        return -1;
      }
      const d1 = fromHexDigit(this.buffer[this.pos]);
      const d2 = fromHexDigit(this.buffer[this.pos + 1]);
      this.pos += 2;
      return (d1 << 4) | d2;
    },
    readNumber() {
      let n = 0;
      let last;
      do {
        const b = this.readByte();
        last = !(b & 0x80);
        n = (n << 7) | (b & 0x7f);
      } while (!last);
      return n;
    },
    readSigned() {
      const n = this.readNumber();
      return n & 1 ? -(n >>> 1) - 1 : n >>> 1;
    },
    readHex(size) {
      const lengthInChars = (size + 1) << 1;
      const s = this.buffer.substring(this.pos, this.pos + lengthInChars);
      this.pos += lengthInChars;
      return s;
    },
    readHexNumber(size) {
      const lengthInChars = (size + 1) << 1,
        stack = [];
      let last;
      do {
        const b = this.readByte();
        last = !(b & 0x80);
        stack.push(b & 0x7f);
      } while (!last);
      let s = "",
        buffer = 0,
        bufferSize = 0;
      while (s.length < lengthInChars) {
        while (bufferSize < 4 && stack.length > 0) {
          buffer |= stack.pop() << bufferSize;
          bufferSize += 7;
        }
        s = toHexDigit(buffer & 15) + s;
        buffer >>= 4;
        bufferSize -= 4;
      }
      return s;
    },
    readHexSigned(size) {
      const num = this.readHexNumber(size);
      const sign = fromHexDigit(num.at(-1)) & 1 ? 15 : 0;
      let c = 0;
      let result = "";
      for (const digit of num) {
        c = (c << 4) | fromHexDigit(digit);
        result += toHexDigit(sign ? (c >> 1) ^ sign : c >> 1);
        c &= 1;
      }
      return result;
    },
    readString() {
      const len = this.readNumber();
      let s = "";
      for (let i = 0; i < len; i++) {
        s += String.fromCharCode(this.readNumber());
      }
      return s;
    },
  };

  const header = reader.readByte();
  const result = {
    type: header >> 1,
    wmode: header & 1,
    comment: null,
    usecmap: null,
    body: [],
  };

  let b;
  while ((b = reader.readByte()) >= 0) {
    const type = b >> 5;
    if (type === 7) {
      switch (b & 0x1f) {
        case 0:
          result.comment = reader.readString();
          break;
        case 1:
          result.usecmap = reader.readString();
          break;
      }
      continue;
    }
    const sequence = !!(b & 0x10);
    const dataSize = b & 15;
    const subitems = [];
    const item = {
      type,
      items: subitems,
    };
    if (sequence) {
      item.sequence = true;
    }
    const ucs2DataSize = 1;
    const subitemsCount = reader.readNumber();
    let start, end, code, char;
    switch (type) {
      case 0:
        start = reader.readHex(dataSize);
        end = addHex(reader.readHexNumber(dataSize), start);
        subitems.push({ start, end });
        for (let i = 1; i < subitemsCount; i++) {
          start = addHex(reader.readHexNumber(dataSize), incHex(end));
          end = addHex(reader.readHexNumber(dataSize), start);
          subitems.push({ start, end });
        }
        break;
      case 1:
        start = reader.readHex(dataSize);
        end = addHex(reader.readHexNumber(dataSize), start);
        code = reader.readNumber();
        subitems.push({ start, end, code });
        for (let i = 1; i < subitemsCount; i++) {
          start = addHex(reader.readHexNumber(dataSize), incHex(end));
          end = addHex(reader.readHexNumber(dataSize), start);
          code = reader.readNumber();
          subitems.push({ start, end, code });
        }
        break;
      case 2:
        char = reader.readHex(dataSize);
        code = reader.readNumber();
        subitems.push({ char, code });
        for (let i = 1; i < subitemsCount; i++) {
          char = sequence
            ? incHex(char)
            : addHex(reader.readHexNumber(dataSize), incHex(char));
          code = reader.readSigned() + (code + 1);
          subitems.push({ char, code });
        }
        break;
      case 3:
        start = reader.readHex(dataSize);
        end = addHex(reader.readHexNumber(dataSize), start);
        code = reader.readNumber();
        subitems.push({ start, end, code });
        for (let i = 1; i < subitemsCount; i++) {
          start = sequence
            ? incHex(end)
            : addHex(reader.readHexNumber(dataSize), incHex(end));
          end = addHex(reader.readHexNumber(dataSize), start);
          code = reader.readNumber();
          subitems.push({ start, end, code });
        }
        break;
      case 4:
        char = reader.readHex(ucs2DataSize);
        code = reader.readHex(dataSize);
        subitems.push({ char, code });
        for (let i = 1; i < subitemsCount; i++) {
          char = sequence
            ? incHex(char)
            : addHex(reader.readHexNumber(ucs2DataSize), incHex(char));
          code = addHex(reader.readHexSigned(dataSize), incHex(code));
          subitems.push({ char, code });
        }
        break;
      case 5:
        start = reader.readHex(ucs2DataSize);
        end = addHex(reader.readHexNumber(ucs2DataSize), start);
        code = reader.readHex(dataSize);
        subitems.push({ start, end, code });
        for (let i = 1; i < subitemsCount; i++) {
          start = sequence
            ? incHex(end)
            : addHex(reader.readHexNumber(ucs2DataSize), incHex(end));
          end = addHex(reader.readHexNumber(ucs2DataSize), start);
          code = reader.readHex(dataSize);
          subitems.push({ start, end, code });
        }
        break;
      default:
        throw new Error("Unknown type: " + type);
    }
    result.body.push(item);
  }

  return result;
}

function toHexDigit(n) {
  return n.toString(16);
}
function fromHexDigit(s) {
  return parseInt(s, 16);
}
function getHexSize(s) {
  return (s.length >> 1) - 1;
}
function writeByte(b) {
  return toHexDigit((b >> 4) & 15) + toHexDigit(b & 15);
}
function writeNumber(n) {
  if (typeof n === "string") {
    let s = "",
      buffer = 0,
      bufferSize = 0;
    let i = n.length;
    while (i > 0) {
      --i;
      buffer |= fromHexDigit(n[i]) << bufferSize;
      bufferSize += 4;
      if (bufferSize >= 7) {
        s = writeByte((buffer & 0x7f) | (s.length > 0 ? 0x80 : 0)) + s;
        buffer >>>= 7;
        bufferSize -= 7;
      }
    }
    if (buffer > 0) {
      s = writeByte((buffer & 0x7f) | (s.length > 0 ? 0x80 : 0)) + s;
    }
    while (s.indexOf("80") === 0) {
      s = s.substring(2);
    }
    return s;
  }
  let s = writeByte(n & 0x7f);
  n >>>= 7;
  while (n > 0) {
    s = writeByte((n & 0x7f) | 0x80) + s;
    n >>>= 7;
  }
  return s;
}
function writeSigned(n) {
  if (typeof n === "string") {
    let t = "";
    let c = fromHexDigit(n[0]);
    const neg = c >= 8;
    c = neg ? c ^ 15 : c;
    for (let i = 1; i < n.length; i++) {
      const d = fromHexDigit(n[i]);
      c = (c << 4) | (neg ? d ^ 15 : d);
      t += toHexDigit(c >> 3);
      c &= 7;
    }
    t += toHexDigit((c << 1) | (neg ? 1 : 0));
    return writeNumber(t);
  }
  return n < 0 ? writeNumber(-2 * n - 1) : writeNumber(2 * n);
}
function writeString(s) {
  let t = writeNumber(s.length);
  for (let i = 0; i < s.length; i++) {
    t += writeNumber(s.charCodeAt(i));
  }
  return t;
}
function addHex(a, b) {
  let c = 0,
    s = "";
  for (let i = a.length - 1; i >= 0; i--) {
    c += fromHexDigit(a[i]) + fromHexDigit(b[i]);
    if (c >= 16) {
      s = toHexDigit(c - 16) + s;
      c = 1;
    } else {
      s = toHexDigit(c) + s;
      c = 0;
    }
  }
  return s;
}
function subHex(a, b) {
  let c = 0,
    s = "";
  for (let i = a.length - 1; i >= 0; i--) {
    c += fromHexDigit(a[i]) - fromHexDigit(b[i]);
    if (c < 0) {
      s = toHexDigit(c + 16) + s;
      c = -1;
    } else {
      s = toHexDigit(c) + s;
      c = 0;
    }
  }
  return s;
}
function incHex(a) {
  let c = 1,
    s = "";
  for (let i = a.length - 1; i >= 0; i--) {
    c += fromHexDigit(a[i]);
    if (c >= 16) {
      s = toHexDigit(c - 16) + s;
      c = 1;
    } else {
      s = toHexDigit(c) + s;
      c = 0;
    }
  }
  return s;
}

function compressCmaps(src, dest, verify) {
  const files = fs.readdirSync(src).filter(function (fn) {
    return !fn.includes("."); // skipping files with the extension
  });
  files.forEach(function (fn) {
    const srcPath = path.join(src, fn);
    const destPath = path.join(dest, fn + ".bcmap");
    const stats = compressCmap(srcPath, destPath, verify);
    console.log(
      "Compressing " +
        fn +
        ": " +
        stats.orig +
        " vs " +
        stats.packed +
        " " +
        ((stats.packed / stats.orig) * 100).toFixed(1) +
        "%"
    );
  });
}

export { compressCmaps };
