importScripts('rlottie-wasm.js');
//import Module, { allocate, intArrayFromString } from './rlottie-wasm';

const _Module = (self as any).Module as any;

const DEFAULT_FPS = 60;

export class RLottieItem {
  private stringOnWasmHeap: any = null;
  private handle: any = null;
  private frameCount = 0;

  private dead = false;
  //private context: OffscreenCanvasRenderingContext2D;

  constructor(private reqId: number, jsString: string, private width: number, private height: number, private fps: number/* , private canvas: OffscreenCanvas */) {
    this.fps = Math.max(1, Math.min(60, fps || DEFAULT_FPS));

    //this.context = canvas.getContext('2d');

    this.init(jsString);

    reply('loaded', this.reqId, this.frameCount, this.fps);

    /* let frame = 0;
    setInterval(() => {
      if(frame >= this.frameCount) frame = 0;
      let _frame = frame++;
      this.render(_frame, null);
    }, 1000 / this.fps); */
  }

  private init(jsString: string) {
    try {
      this.handle = worker.Api.init();
  
      // @ts-ignore
      this.stringOnWasmHeap = allocate(intArrayFromString(jsString), 'i8', 0);
  
      this.frameCount = worker.Api.loadFromData(this.handle, this.stringOnWasmHeap);
  
      worker.Api.resize(this.handle, this.width, this.height);
    } catch(e) {
      console.error('init RLottieItem error:', e);
      reply('error', this.reqId, e);
    }
  }

  public render(frameNo: number, clamped: Uint8ClampedArray) {
    if(this.dead) return;
    //return;
  
    if(this.frameCount < frameNo || frameNo < 0) {
      return;
    }
  
    try {
      worker.Api.render(this.handle, frameNo);
  
      var bufferPointer = worker.Api.buffer(this.handle);
  
      var data = _Module.HEAPU8.subarray(bufferPointer, bufferPointer + (this.width * this.height * 4));
  
      if(!clamped) {
        clamped = new Uint8ClampedArray(data);
      } else {
        clamped.set(data);
      }

      //this.context.putImageData(new ImageData(clamped, this.width, this.height), 0, 0);
  
      reply('frame', this.reqId, frameNo, clamped);
    } catch(e) {
      console.error('Render error:', e);
      this.dead = true;
      reply('error', this.reqId, e);
    }
  }

  public destroy() {
    this.dead = true;

    worker.Api.destroy(this.handle);
  }
}

class RLottieWorker {
  public Api: any = {};

  public initApi() {
    this.Api = {
      init: _Module.cwrap('lottie_init', '', []),
      destroy: _Module.cwrap('lottie_destroy', '', ['number']),
      resize: _Module.cwrap('lottie_resize', '', ['number', 'number', 'number']),
      buffer: _Module.cwrap('lottie_buffer', 'number', ['number']),
      render: _Module.cwrap('lottie_render', '', ['number', 'number']),
      loadFromData: _Module.cwrap('lottie_load_from_data', 'number', ['number', 'number']),
    };
  }

  public init() {
    this.initApi();
    reply('ready');
  }
}

const worker = new RLottieWorker();

_Module.onRuntimeInitialized = function() {
  worker.init();
};

const items: {[reqId: string]: RLottieItem} = {};
const queryableFunctions = {
  loadFromData: function(reqId: number, jsString: string, width: number, height: number/* , canvas: OffscreenCanvas */) {
    try {
      // ! WARNING, с этой проверкой не все стикеры работают, например - ДУРКА
      /* if(!/"tgs":\s*?1./.test(jsString)) {
        throw new Error('Invalid file');
      } */

      const match = jsString.match(/"fr":\s*?(\d+?),/);
      const frameRate = +match?.[1] || DEFAULT_FPS;

      //console.log('Rendering sticker:', reqId, frameRate, 'now rendered:', Object.keys(items).length);

      items[reqId] = new RLottieItem(reqId, jsString, width, height, frameRate/* , canvas */);
    } catch(e) {
      console.error('Invalid file for sticker:', jsString);
      reply('error', reqId, e);
    }
  },
  destroy: function(reqId: number) {
    if(!items.hasOwnProperty(reqId)) {
      return;
    }

    items[reqId].destroy();
    delete items[reqId];
  },
  renderFrame: function(reqId: number, frameNo: number, clamped: Uint8ClampedArray) {
    //console.log('worker renderFrame', reqId, frameNo, clamped);
    items[reqId].render(frameNo, clamped);
  }
};

function defaultReply(message: any) {
  // your default PUBLIC function executed only when main page calls the queryableWorker.postMessage() method directly
  // do something
}

/**
 * Returns true when run in WebKit derived browsers.
 * This is used as a workaround for a memory leak in Safari caused by using Transferable objects to
 * transfer data between WebWorkers and the main thread.
 * https://github.com/mapbox/mapbox-gl-js/issues/8771
 *
 * This should be removed once the underlying Safari issue is fixed.
 *
 * @private
 * @param scope {WindowOrWorkerGlobalScope} Since this function is used both on the main thread and WebWorker context,
 *      let the calling scope pass in the global scope object.
 * @returns {boolean}
 */
let _isSafari: boolean = null;
function isSafari(scope: any) {
  if(_isSafari == null) {
    const userAgent = scope.navigator ? scope.navigator.userAgent : null;
    _isSafari = !!scope.safari ||
    !!(userAgent && (/\b(iPad|iPhone|iPod)\b/.test(userAgent) || (!!userAgent.match('Safari') && !userAgent.match('Chrome'))));
  }
  return _isSafari;
}

function reply(...args: any[]) {
  if(arguments.length < 1) { 
    throw new TypeError('reply - not enough arguments'); 
  }

  //if(arguments[0] == 'frame') return;

  var args = Array.prototype.slice.call(arguments, 1);
  if(isSafari(self)) {
    postMessage({ 'queryMethodListener': arguments[0], 'queryMethodArguments': args });
  } else {
    var transfer = [];
    for(var i = 0; i < args.length; i++) {
      if(args[i] instanceof ArrayBuffer) {
        transfer.push(args[i]);
      }
  
      if(args[i].buffer && args[i].buffer instanceof ArrayBuffer) {
        transfer.push(args[i].buffer);
        //args[i] = args[i].buffer;
      }
    }

    postMessage({ 'queryMethodListener': arguments[0], 'queryMethodArguments': args }, transfer);
  }

  //postMessage({ 'queryMethodListener': arguments[0], 'queryMethodArguments': Array.prototype.slice.call(arguments, 1) });
  //console.error(transfer, args);
}

onmessage = function(oEvent) {
  if(oEvent.data instanceof Object && oEvent.data.hasOwnProperty('queryMethod') && oEvent.data.hasOwnProperty('queryMethodArguments')) {
    // @ts-ignore
    queryableFunctions[oEvent.data.queryMethod].apply(self, oEvent.data.queryMethodArguments);
  } else {
    defaultReply(oEvent.data);
  }
};
