/*
 * Copyright (c) 2021 Antoine Martin <antoine@xpra.org>
 */

importScripts("./lib/lz4.js");
importScripts("./RgbHelpers.js");

const on_hold = new Map();

let zerocopy = true;

function decode_eos(wid) {}

function decode_draw_packet(packet, start) {
  const wid = packet[1];
  const width = packet[4];
  const height = packet[5];
  const coding = packet[6];
  const packet_sequence = packet[8];

  function send_back(raw_buffers) {
    const wid_hold = on_hold.get(wid);
    if (wid_hold) {
      //find the highest sequence number which is still lower than this packet
      let seq_holding = 0;
      for (const seq of wid_hold.keys()) {
        if (seq > seq_holding && seq < packet_sequence) {
          seq_holding = seq;
        }
      }
      if (seq_holding) {
        const held = wid_hold.get(seq_holding);
        if (held) {
          held.push([packet, raw_buffers]);
          return;
        }
      }
    }
    do_send_back(packet, raw_buffers);
  }

  function do_send_back(p, raw_buffers) {
    self.postMessage({
      draw: p,
      start
    }, raw_buffers);
  }

  function decode_error(message) {
    self.postMessage({
      error: `${message}`,
      packet,
      start
    });
  }

  function hold() {
    //we're loading asynchronously
    //so ensure that any packet sequence arriving after this one will be put on hold
    //until we have finished decoding this one:
    let wid_hold = on_hold.get(wid);
    if (!wid_hold) {
      wid_hold = new Map();
      on_hold.set(wid, wid_hold);
    }
    wid_hold.set(packet_sequence, []);
    return wid_hold;
  }

  function release() {
    const wid_hold = on_hold.get(wid);
    if (!wid_hold) {
      //could have been cancelled by EOS
      return;
    }
    //release any packets held back by this image:
    const held = wid_hold.get(packet_sequence);
    if (!held) {
      //could have been cancelled by EOS
      return;
    }
    let index;
    for (index = 0; index < held.length; index++) {
      const held_packet = held[index][0];
      const held_raw_buffers = held[index][1];
      do_send_back(held_packet, held_raw_buffers);
    }
    wid_hold.delete(packet_sequence);
    if (wid_hold.size === 0 && on_hold.has(wid)) {
      //this was the last held sequence for this window
      on_hold.delete(wid);
    }
  }

  function send_rgb32_back(data, actual_width, actual_height, options) {
    const img = new ImageData(
      new Uint8ClampedArray(data.buffer),
      actual_width,
      actual_height
    );
    hold();
    createImageBitmap(img, 0, 0, actual_width, actual_height, options).then(
      function(bitmap) {
        packet[6] = "bitmap:rgb32";
        packet[7] = bitmap;
        send_back([bitmap]);
        release();
      },
      function(error) {
        decode_error(
          `failed to create ${actual_width}x${actual_height} rgb32 bitmap from buffer ${data}: ${error}`
        );
        release();
      }
    );
  }

  let options = {};
  if (packet.length > 10) options = packet[10];
  const bitmap_options = {
    premultiplyAlpha: "none",
  };
  const scaled_size = options["scaled_size"];
  if (scaled_size) {
    delete options["scaled-size"];
    bitmap_options["resizeWidth"] = width;
    bitmap_options["resizeHeight"] = height;
    bitmap_options["resizeQuality"] = "medium";
  }
  try {
    if (coding === "rgb24" || coding === "rgb32") {
      const data = decode_rgb(packet);
      send_rgb32_back(data, width, height, bitmap_options);
    } else if (
      coding.startsWith("png") ||
      coding === "jpeg" ||
      coding === "webp" ||
      coding === "avif"
    ) {
      const data = packet[7];
      if (!data.buffer) {
        decode_error(`missing pixel data buffer: ${typeof data}`);
        release();
        return;
      }
      let buffer = data;
      if (zerocopy) {
        buffer = data.buffer;
      }
      const blob = new Blob([buffer], {
        type: `image/${coding}`
      });
      hold();
      createImageBitmap(blob, bitmap_options).then(
        function(bitmap) {
          packet[6] = `bitmap:${coding}`;
          packet[7] = bitmap;
          send_back([bitmap]);
          release();
        },
        function(error) {
          console.info(`decode worker failed to create ${coding} image bitmap: ${error}`);
          console.info(`using ${blob} + ${JSON.stringify(bitmap_options)} from data=${data}`);
          console.info(`data from ${buffer.constructor.name} of length ${data.length}`);
          // maybe the regular paint function will succeed?
          console.info("sending it back for decoding directly in the client");
          send_back([]);
          release();
          if (zerocopy) {
            console.warn("turning off zerocopy");
            zerocopy = false;
          }
        }
      );
    } else {
      //pass-through:
      send_back([]);
    }
  } catch (error) {
    decode_error(
      `error processing ${coding} packet ${packet_sequence}: ${error}`
    );
  }
}

function check_image_decode(
  format,
  image_bytes,
  success_callback,
  fail_callback
) {
  if (console) {
    // console.info(
    //   "checking",
    //   format,
    //   `with test image: ${image_bytes.length} bytes`
    // );
  }
  try {
    const timer = setTimeout(function() {
      fail_callback(format, `timeout, no ${format} picture decoded`);
    }, 2000);
    const data = new Uint8Array(image_bytes);
    const blob = new Blob([data], {
      type: `image/${format}`
    });
    createImageBitmap(blob, {
      premultiplyAlpha: "none",
    }).then(
      function() {
        clearTimeout(timer);
        success_callback(format);
      },
      function(error) {
        clearTimeout(timer);
        fail_callback(format, `${error}`);
      }
    );
  } catch (error) {
    fail_callback(format, `${error}`);
  }
}

onmessage = function(e) {
  const data = e.data;
  switch (data.cmd) {
    case "check": {
      const encodings = data.encodings;
      if (console) {
        console.info("decode worker checking:", encodings);
      }
      const CHECKS = {
        png: [
          137, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82, 0, 0, 0,
          1, 0, 0, 0, 1, 8, 6, 0, 0, 0, 31, 21, 196, 137, 0, 0, 0, 13, 73, 68,
          65, 84, 120, 218, 99, 252, 207, 192, 80, 15, 0, 4, 133, 1, 128, 132,
          169, 140, 33, 0, 0, 0, 0, 73, 69, 78, 68, 174, 66, 96, 130,
        ],
        webp: [
          82, 73, 70, 70, 58, 0, 0, 0, 87, 69, 66, 80, 86, 80, 56, 32, 46, 0, 0,
          0, 178, 2, 0, 157, 1, 42, 2, 0, 2, 0, 46, 105, 52, 154, 77, 34, 34,
          34, 34, 34, 0, 104, 75, 40, 0, 5, 206, 150, 90, 0, 0, 254, 247, 159,
          127, 253, 15, 63, 198, 192, 255, 242, 240, 96, 0, 0,
        ],
        jpeg: [
          255, 216, 255, 224, 0, 16, 74, 70, 73, 70, 0, 1, 1, 1, 0, 96, 0, 96,
          0, 0, 255, 219, 0, 67, 0, 8, 6, 6, 7, 6, 5, 8, 7, 7, 7, 9, 9, 8, 10,
          12, 20, 13, 12, 11, 11, 12, 25, 18, 19, 15, 20, 29, 26, 31, 30, 29,
          26, 28, 28, 32, 36, 46, 39, 32, 34, 44, 35, 28, 28, 40, 55, 41, 44,
          48, 49, 52, 52, 52, 31, 39, 57, 61, 56, 50, 60, 46, 51, 52, 50, 255,
          219, 0, 67, 1, 9, 9, 9, 12, 11, 12, 24, 13, 13, 24, 50, 33, 28, 33,
          50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50,
          50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50,
          50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 255,
          192, 0, 17, 8, 0, 1, 0, 1, 3, 1, 34, 0, 2, 17, 1, 3, 17, 1, 255, 196,
          0, 31, 0, 0, 1, 5, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3,
          4, 5, 6, 7, 8, 9, 10, 11, 255, 196, 0, 181, 16, 0, 2, 1, 3, 3, 2, 4,
          3, 5, 5, 4, 4, 0, 0, 1, 125, 1, 2, 3, 0, 4, 17, 5, 18, 33, 49, 65, 6,
          19, 81, 97, 7, 34, 113, 20, 50, 129, 145, 161, 8, 35, 66, 177, 193,
          21, 82, 209, 240, 36, 51, 98, 114, 130, 9, 10, 22, 23, 24, 25, 26, 37,
          38, 39, 40, 41, 42, 52, 53, 54, 55, 56, 57, 58, 67, 68, 69, 70, 71,
          72, 73, 74, 83, 84, 85, 86, 87, 88, 89, 90, 99, 100, 101, 102, 103,
          104, 105, 106, 115, 116, 117, 118, 119, 120, 121, 122, 131, 132, 133,
          134, 135, 136, 137, 138, 146, 147, 148, 149, 150, 151, 152, 153, 154,
          162, 163, 164, 165, 166, 167, 168, 169, 170, 178, 179, 180, 181, 182,
          183, 184, 185, 186, 194, 195, 196, 197, 198, 199, 200, 201, 202, 210,
          211, 212, 213, 214, 215, 216, 217, 218, 225, 226, 227, 228, 229, 230,
          231, 232, 233, 234, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250,
          255, 196, 0, 31, 1, 0, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0,
          1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 255, 196, 0, 181, 17, 0, 2, 1, 2,
          4, 4, 3, 4, 7, 5, 4, 4, 0, 1, 2, 119, 0, 1, 2, 3, 17, 4, 5, 33, 49, 6,
          18, 65, 81, 7, 97, 113, 19, 34, 50, 129, 8, 20, 66, 145, 161, 177,
          193, 9, 35, 51, 82, 240, 21, 98, 114, 209, 10, 22, 36, 52, 225, 37,
          241, 23, 24, 25, 26, 38, 39, 40, 41, 42, 53, 54, 55, 56, 57, 58, 67,
          68, 69, 70, 71, 72, 73, 74, 83, 84, 85, 86, 87, 88, 89, 90, 99, 100,
          101, 102, 103, 104, 105, 106, 115, 116, 117, 118, 119, 120, 121, 122,
          130, 131, 132, 133, 134, 135, 136, 137, 138, 146, 147, 148, 149, 150,
          151, 152, 153, 154, 162, 163, 164, 165, 166, 167, 168, 169, 170, 178,
          179, 180, 181, 182, 183, 184, 185, 186, 194, 195, 196, 197, 198, 199,
          200, 201, 202, 210, 211, 212, 213, 214, 215, 216, 217, 218, 226, 227,
          228, 229, 230, 231, 232, 233, 234, 242, 243, 244, 245, 246, 247, 248,
          249, 250, 255, 218, 0, 12, 3, 1, 0, 2, 17, 3, 17, 0, 63, 0, 247, 250,
          40, 162, 128, 63, 255, 217,
        ],
      };
      const errors = [];
      const formats = ["rgb24", "rgb32"];
      const done = (format) => {
        delete CHECKS[format];
        if (Object.keys(CHECKS).length === 0) {
          if (errors.length === 0) {
            self.postMessage({
              result: true,
              formats
            });
          } else {
            self.postMessage({
              result: false,
              errors
            });
          }
        }
      };
      const success = (format) => {
        //only enable this format if the client requested it:
        if (encodings.includes(format)) {
          formats.push(format);
        }
        done(format);
      };
      const failure = (format, message) => {
        //only record an error if the client actually asked us to verify this format
        if (encodings.includes(format)) {
          errors.push(message);
          if (console.warn) {
            console.warn(
              `Warning: decode worker error on '${format}': ${message}`
            );
          }
        } else {
          console.info(`decode worker failure on '${format}': ${message}`);
        }
        done(format);
      };
      for (const format in CHECKS) {
        const image_bytes = CHECKS[format];
        check_image_decode(format, image_bytes, success, failure);
      }
      break;
    }
    case "eos":
      decode_eos(data.wid);
      break;
    case "remove":
      decode_eos(data.wid);
      on_hold.delete(data.wid);
      break;
    case "decode":
      decode_draw_packet(data.packet, data.start);
      break;
    case "close":
      on_hold.clear();
      break;
    default:
      console.error(`decode worker got unknown message: ${data.cmd}`);
  }
};
