import { Logger as internalLogger, formatTime, shortcutKeys, sliceTime } from './utils';
import md5 from 'js-md5';

const INVAILD = 0x00;
const QUERY_DEVICE_INFO = 0x01;
const QUERY_DEVICE_TIME = 0x02;
const SET_DEVICE_TIME = 0x03;
const QUERY_FILE_LIST = 0x04;
const TRANSFER_FILE = 0x05;
const QUERY_FILE_COUNT = 0x06;
const DELETE_FILE = 0x07;
const REQUEST_FIRMWARE_UPGRADE = 0x08;
const FIRMWARE_UPLOAD = 0x09;
const READ_CARD_INFO = 0x10;
const FORMAT_CARD = 0x11;
const GET_RECORDING_FILE = 0x12;
const RESTORE_FACTORY_SETTINGS = 0x13;
const SCHEDULE_INFO = 0x14;
const DEVICE_MSG_TEST = 0x0a;
const BNC_DEMO_TEST = 0x0a;
const GET_SETTINGS = 0x0b;
const SET_SETTINGS = 0x0c;
const GET_FILE_BLOCK = 0x0d;
const TRANSFER_FILE_PARTIAL = 0x15;
const REQUEST_TONE_UPDATE = 0x16;
const TONE_UPDATE = 0x17;
const REQUEST_UAC_UPDATE = 0x18;
const UAC_UPDATE = 0x19;
const FACTORY_RESET = 0xf00b;
const GET_BATTERY_STATUS = 0x1004;
const REALTIME_READ_SETTING = 0x20;
const REALTIME_CONTROL = 0x21;
const REALTIME_TRANSFER = 0x22;

const BLUETOOTH_SCAN = 0x1001;
const BLUETOOTH_CMD = 0x1002;
const BLUETOOTH_STATUS = 0x1003;

const TEST_SN_WRITE = 0xf007;
const RECORD_TEST_START = 0xf008;
const RECORD_TEST_END = 0xf009;

const COMMAND_NAMES = {
  [INVAILD]: 'invalid-0',
  [QUERY_DEVICE_INFO]: 'get-device-info',
  [QUERY_DEVICE_TIME]: 'get-device-time',
  [SET_DEVICE_TIME]: 'set-device-time',
  [QUERY_FILE_LIST]: 'get-file-list',
  [TRANSFER_FILE]: 'transfer-file',
  [QUERY_FILE_COUNT]: 'get-file-count',
  [DELETE_FILE]: 'delete-file',
  [REQUEST_FIRMWARE_UPGRADE]: 'request-firmware-upgrade',
  [FIRMWARE_UPLOAD]: 'firmware-upload',
  [READ_CARD_INFO]: 'read card info',
  [FORMAT_CARD]: 'format card',
  [GET_RECORDING_FILE]: 'get recording file',
  [RESTORE_FACTORY_SETTINGS]: 'restore factory settings',
  [SCHEDULE_INFO]: 'send meeting schedule info',
  [DEVICE_MSG_TEST]: 'device msg test',
  [BNC_DEMO_TEST]: 'bnc demo test',
  [GET_SETTINGS]: 'get-settings',
  [SET_SETTINGS]: 'set-settings',
  [GET_FILE_BLOCK]: 'get file block',
  [FACTORY_RESET]: 'factory reset',
  [TEST_SN_WRITE]: 'test sn write',
  [RECORD_TEST_START]: 'record test start',
  [RECORD_TEST_END]: 'record test end',
  [BLUETOOTH_SCAN]: 'bluetooth-scan',
  [BLUETOOTH_CMD]: 'bluetooth-cmd',
  [BLUETOOTH_STATUS]: 'bluetooth-status'
};

let Logger = null;

function Jensen(log, conn) {
  Logger = log || internalLogger;
  let device = conn;
  let actions = {};
  let buffer = [];
  let blocks = [];
  let sequence = 0;
  let current = null;
  let commands = [];
  let statusTimeout = null;
  let recv = false;
  let ready = false;
  let totalBytes = 0;
  let handlers = {};
  let pid = 0;
  let recvable = true;

  let self = this;
  this.data = {};
  this.sqidx = new Date().getTime();
  // 消息
  this.decodeTimeout = 0;
  this.timewait = 1;

  this.ondisconnect = null;
  this.isStopConnectionCheck = false;
  this.onconnect = null;
  this.onreceive = null;

  const RECV_BUFF_SIZE = 51200;

  const _check_conn_status = () => {
    if (device?.opened === false) {
      try {
        clearTimeout(statusTimeout);
        const audio = document.getElementById('test_audio');
        audio && audio.remove();
        if (this.ondisconnect && !this.isStopConnectionCheck) this.ondisconnect();
      } catch (e) {
        console.log(e);
      }
    }
    statusTimeout = setTimeout(() => {
      _check_conn_status();
    }, 100);
  };

  const crash = async function (procedure, error) {
    Logger.error('jensen', procedure, String(error));
    self.versionCode = null;
    self.versionNumber = null;
  };

  function determineModel(productId) {
    if (productId == 0xb00c) return 'hidock-h1';
    if (productId == 0xb00d) return 'hidock-h1e';
    if (productId == 0xb00e) return 'hidock-p1';
    if (productId == 0xb00f) return 'hidock-p1:mini';
    if (productId == 0x0100) return 'hidock-h1';
    if (productId == 0x0101) return 'hidock-h1e';
    if (productId == 0x0102) return 'hidock-h1';
    if (productId == 0x0103) return 'hidock-h1e';
    if (productId == 0x2040) return 'hidock-p1';
    if (productId == 0x2041) return 'hidock-p1:mini';
    return 'unknown';
  }

  const setup = async function (disableOnConnect) {
    if (ready) return;
    self.versionCode = null;
    self.versionNumber = null;
    commands.length = 0;
    try {
      await device.selectConfiguration(1);
      await device.claimInterface(0);
      await device.selectAlternateInterface(0, 0);
      pid = device.productId;
      self.model = determineModel(device.productId);
      Logger.info('jensen', 'connect', 'device pid: ' + device.productId);
    } catch (e) {
      Logger.error('jensen', 'setup', String(e));
    }
    if (!disableOnConnect) _check_conn_status();
    current = null;
    recv = false;
    ready = true;
    Logger.debug('jensen', 'setup', 'setup webusb connection');
    try {
      if (!disableOnConnect && !self.isStopConnectionCheck) self.onconnect?.();
    } catch (err) {
      Logger.error('jensen', 'setup', err);
    }
  };

  this.initialize = async function () {
    if (conn) {
      if (!conn.opened) conn.open();
      await setup();
    }
    return true;
  }

  this.sequence = function () {
    return this.sqidx++;
  }

  this.reconnect = async function () {
    try {
      await device.close();
      await device.open();
      ready = false;
      await setup();
      return true;
    }
    catch (ex) {
      Logger.error('jensen', 'reconnect', ex);
    }
  }

  this.connect = async function () {
    Logger.debug('jensen', 'connect', 'connect');
    let r = await self.tryconnect();
    if (r) return;

    let conn = await navigator.usb.requestDevice({
      filters: [{ vendorId: 0x10d6 }, { vendorId: 0x3887 }]
    });
    await conn.open();
    // self.model = conn.productId == 45069 ? 'hidock-h1e' : 'hidock-h1';
    device = conn;
    await setup();
  };

  this.getModel = function () {
    return this.model;
  };

  this.init = async function _init() {
    if (!navigator.usb) {
      Logger.error('jensen', 'init', 'webusb not supported');
      return;
    }
    navigator.usb.onconnect = function (e) {
      self.tryconnect();
    };
    await self.connect();
  };

  this.tryconnect = async function (disableOnConnect) {
    await this.disconnect();
    let devices = await navigator.usb.getDevices();
    for (let i = 0; i < devices.length; i++) {
      let item = devices[i];
      if (item.productName.indexOf('HiDock') > -1) {
        Logger.debug('jensen', 'tryconnect', 'detected: ' + item.productName);
        await item.open();
        device = item;
        await setup(disableOnConnect);
        return true;
      }
    }
    Logger.debug('jensen', 'tryconnect', 'no HiDock found');
    return false;
  };

  this.getUSBDevice = function () {
    return device;
  }

  this.setUSBDevice = function (dev) {
    device = dev;
    ready = false;
    setup();
  }

  this.stopReceive = function () {
    recvable = false;
  }

  this.resumeReceive = async function () {
    recvable = true;
    if (current) trigger(null, current.replace(/^cmd-(\d+)-(\d+)$/gi, '$1'));
    blocks.length = 0;
    totalBytes = 0;
    // 不发送一下，是不会回应数据的
    await this.getTime(5);
  }

  this.isConnected = function () {
    return device != null;
  };

  this.disconnect = async function () {
    Logger.info('jensen', 'disconnect', 'disconnect');
    try {
      await device?.close();
    } catch (e) { }
  };

  this.send = function (cmd, seconds, onprogress) {
    cmd.sequence(sequence++);
    cmd.onprogress = onprogress;
    if (seconds) cmd.expireAfter(seconds);
    commands.push(cmd);
    sendNext();
    return register(cmd, seconds);
  };

  const sendNext = async function () {
    if (current) {
      // Logger.info('jensen', 'sendNext', 'return cuz current is: ' + current);
      return;
    }

    let cmd = null;
    let now = new Date().getTime();
    while (true) {
      if (commands.length == 0) return;
      cmd = commands.shift();
      if (cmd.expireTime > 0 && cmd.expireTime < now) {
        Logger.info('jensen', 'sendNext', 'expired: cmd-' + cmd.command + '-' + cmd.index + ', ' + cmd.command);
        continue;
      }
      break;
    }

    let data = cmd.make();
    current = 'cmd-' + cmd.command + '-' + cmd.index;
    Logger.debug('jensen', 'sendNext', pid + '-command: ' + cmd.command + ', data bytes: ' + data.byteLength);

    self.timewait = cmd.command == TRANSFER_FILE || cmd.command == GET_FILE_BLOCK ? 1000 : 10;
    console.log(pid + 'Send', data);
    await device.transferOut(1, data).catch((e) => crash('sendNext', e));
    if (cmd.onprogress) cmd.onprogress(1, 1);
    /*
        if (data.byteLength < 1000) await device.transferOut(1, data).catch((e) => crash('sendNext', e));
        else
        {
            let blks = Math.ceil(data.byteLength / 1000);
            Logger.info('jensen', 'sendNext', blks + ' blocks / ' + data.byteLength + ' bytes');
            for (let i = 0, k = blks; i < k; i++)
            {
                var start = i * 1000;
                var end = (i + 1) * 1000;
                await device.transferOut(1, data.subarray(start, end)).then(() =>
                {
                    Logger.debug('jensen', 'sendNext', i + ' / ' + blks);
                    if (cmd.onprogress)
                    {
                        cmd.onprogress(Math.min(end, data.byteLength), data.byteLength);
                    }
                })
                .catch((e) => crash('sendNext', e));
            }
        }
        */
    totalBytes = 0;
    if (recv == false) tryReceive();
    else recv = true;
  };

  const register = function (cmd, seconds) {
    let tag = 'cmd-' + cmd.command + '-' + cmd.index;
    let t = seconds
      ? setTimeout(() => {
        timeout(tag);
      }, seconds * 1000)
      : null;
    return new Promise((resolve, reject) => {
      actions[tag] = {
        tag: tag,
        resolve: resolve,
        reject: reject,
        timeout: t
      };
    });
  };

  this._trigger = function (resp, msgid) {
    trigger(resp, msgid);
  }

  const trigger = function (resp, msgid) {
    if (current == null) return;
    Logger.debug('jensen', 'trigger', pid + ':trigger - ' + current.substring(0, current.lastIndexOf('-')) + ' <---> cmd-' + (msgid || 'err'));
    if (current.substring(0, current.lastIndexOf('-')) != 'cmd-' + msgid) {
      // current = null;
      Logger.debug('jensen', 'trigger', 'msgid mismatched');
      return;
    }
    if (current in actions == false) {
      Logger.debug('jensen', 'trigger', pid + ':no action registered');
      return;
    }
    let p = actions[current];
    if (p.timeout) clearTimeout(p.timeout);
    p.resolve(resp);
    delete actions[current];
    current = null;
  };

  const timeout = function (tag) {
    Logger.debug('jensen', 'timeout', 'timeout ' + tag);
    let p = actions[tag];
    p.resolve(null);
    delete actions[tag];
    current = null;
  };

  this.dump = function () {
    console.log('actions', actions);
    console.log('pending commands', commands);
    console.log('current', current);
    console.log('device', device);
  };

  const tryReceive = function () {
    if (device && recvable)
      // console.log('trace', device);
      device.transferIn(2, RECV_BUFF_SIZE).then((r) => {
        Logger.save?.('jensen', 'tryReceive', r?.data);
        console.log(pid + ' Receive', r?.data);
        receive(r);
      });
  };

  const read_int = function (a, b, c, d) {
    if (arguments.length === 2) {
      return ((a & 0xff) << 8) | (b & 0xff);
    } else if (arguments.length === 4) {
      return ((a & 0xff) << 24) | ((b & 0xff) << 16) | ((c & 0xff) << 8) | (d & 0xff);
    }
  };

  const receive = function (result) {
    totalBytes += result.data.byteLength;
    // 做一个回调，怎么样找到它呢？
    blocks.push(result.data);
    tryReceive();
    if (self.decodeTimeout) clearTimeout(self.decodeTimeout);
    self.decodeTimeout = setTimeout(function () {
      tryDecode();
    }, self.timewait);
    if (self.onreceive) {
      try {
        self.onreceive(totalBytes);
      } catch (e) { }
    }
  };

  const tryDecode = function () {
    if (!current) return;
    // 一个容器，比单独任意一个小包要大一点儿，好像还差一点儿
    let stime = new Date();
    let buff = new ArrayBuffer(RECV_BUFF_SIZE * 2);
    let bview = new Uint8Array(buff);
    // buff中实际使用的字节数，有效字节数
    let buffLength = 0;
    let crash = false;
    for (let i = 0, l = blocks.length; i < l; i++) {
      let block = blocks.shift();
      // 把block加到buff的最末尾
      for (let k = 0; k < block.byteLength; k++) {
        bview[k + buffLength] = block.getInt8(k);
      }
      buffLength += block.byteLength;

      let startIndex = 0;
      while (true) {
        let rst = null;
        try {
          rst = decodeMessage(bview, startIndex, buffLength);
        } catch (e) {
          Logger.error('jensen', 'decode', String(e));
          crash = true;
          break;
        }
        if (rst == null) {
          break;
        }
        startIndex += rst.length;

        let msg = rst.message;
        // WARN: 接下来怎么整
        let cname = msg.id === FACTORY_RESET ? 'factory-reset' : COMMAND_NAMES[msg.id];
        let heading = [];
        for (let x = 0; x < msg.body?.byteLength && x < 32; x++) {
          heading.push('0' + (msg.body[x] & 0xff).toString(16).replace(/^0(\w{2})$/gi, '$1'));
        }
        if (msg.id !== TRANSFER_FILE)
          Logger.debug(
            'jensen',
            'receive',
            pid + '-recv: ' + cname + ', seq: ' + msg.sequence + ', data bytes: ' + msg.body?.byteLength + ', data: ' + heading.join(' ')
          );
        try {
          let handler = handlers[msg.id];
          if (!handler) handler = Jensen.handlers[msg.id];
          let r = handler(msg, self);
          if (r) trigger(r, msg.id);
        } catch (e) {
          console.error(e);
          trigger(e);
          Logger.error('jensen', 'receive', pid + '-recv: ' + msg.id + ', seq: ' + msg.sequence + ', error: ' + String(e));
        }
        sendNext();
      }
      // 是否已经崩溃了？
      if (crash) {
        // cmd-5-42
        // 5是msgid
        let msgid = parseInt(current.replace(/^cmd-(\d+)-(\d+)$/gi, '$1'));
        try {
          let handler = handlers[msgid];
          if (!handler) handler = Jensen.handlers[msgid];
          handler(null, self);
        } catch (e) {
          console.error(e);
          trigger(e);
          Logger.error('jensen', 'decode', 'decode error: ' + String(e));
        }
        trigger(null, msgid);
        blocks.length = 0;
        break;
      }

      // WARN: 需要把剩余的字节数挪到最前面去
      // startIndex已经抵达最后的位置，startIndex -> buffLength就是需要移到最前面去的字节内容了
      for (let k = 0, bs = buffLength - startIndex; k < bs; k++) {
        bview[k] = bview[k + startIndex];
      }
      buffLength = buffLength - startIndex;
    }
    // console.error('Decode: ' + (new Date().getTime() - stime.getTime()) + 'ms');
  };

  const decodeMessage = function (dataView, startIndex, buffLength) {
    let dataLen = buffLength - startIndex;
    if (dataLen < 12) return null;
    if (dataView[startIndex + 0] !== 0x12 || dataView[startIndex + 1] !== 0x34) throw new Error('invalid header');
    // 2 字节的指令id
    let idx = 2;
    // let cmdid = this.nextShort(idx);
    let cmdid = read_int(dataView[startIndex + idx], dataView[startIndex + idx + 1]);
    idx += 2;
    let sequeue = read_int(
      dataView[startIndex + idx + 0],
      dataView[startIndex + idx + 1],
      dataView[startIndex + idx + 2],
      dataView[startIndex + idx + 3]
    );
    idx += 4;
    let len = read_int(
      dataView[startIndex + idx + 0],
      dataView[startIndex + idx + 1],
      dataView[startIndex + idx + 2],
      dataView[startIndex + idx + 3]
    );
    let padding = (len >> 24) & 0xff;
    len = len & 0xffffff;
    idx += 4;
    // 需要去除的字节数
    var cutLen = 0;
    // 数据还没有完全准备好
    if (dataLen < 12 + len + padding) return null;
    // 去掉header部分
    // 下面这一行做什么用的？
    // for (let i = 0; i < 12; i++) this.buffer[i + cutLen];
    cutLen += 12;

    // 取走body部分
    // 数据体部分
    // let body = new Uint8Array(new ArrayBuffer(len));
    // for (let i = 0; i < len; i++) body.push(this.buffer[i + cutLen]);
    let body = dataView.slice(startIndex + cutLen, startIndex + cutLen + len);
    cutLen += len;

    // 干掉补上来的数据
    // 下面这一行做什么用的？
    // for (let i = 0; i < padding; i++) this.buffer[i + cutLen];
    cutLen += padding;
    // self.buffer = self.buffer.slice(cutLen);
    return { message: new Message(cmdid, sequeue, body), length: cutLen };
  };

  this.to_bcd = function (str) {
    let x = [];
    for (let i = 0; i < str.length; i += 2) {
      let h = (str.charCodeAt(i) - 48) & 0xff;
      let l = (str.charCodeAt(i + 1) - 48) & 0xff;
      x.push((h << 4) | l);
    }
    return x;
  };

  this.from_bcd = function () {
    let str = '';
    for (let i = 0; i < arguments.length; i++) {
      let v = arguments[i] & 0xff;
      str += (v >> 4) & 0x0f;
      str += v & 0x0f;
    }
    return str;
  };

  this.registerHandler = function (cmdid, handler) {
    handlers[cmdid] = handler;
  };
}

function Command(id) {
  this.command = id;
  this.msgBody = [];
  this.index = 0;
  this.expireTime = 0;
  this.timeout = 0;
  this.body = function (data) {
    this.msgBody = data;
    return this;
  };
  this.expireAfter = function (seconds) {
    this.expireTime = new Date().getTime() + seconds * 1000;
  };
  this.sequence = function (seq) {
    this.index = seq;
    return this;
  };
  this.make = function () {
    let msg = new Uint8Array(2 + 2 + 4 + 4 + this.msgBody.length);
    let idx = 0;
    // msg header
    msg[idx++] = 0x12;
    msg[idx++] = 0x34;
    // msg command id
    msg[idx++] = (this.command >> 8) & 0xff;
    msg[idx++] = (this.command >> 0) & 0xff;
    // msg sequence id
    msg[idx++] = (this.index >> 24) & 0xff;
    msg[idx++] = (this.index >> 16) & 0xff;
    msg[idx++] = (this.index >> 8) & 0xff;
    msg[idx++] = (this.index >> 0) & 0xff;
    // msg body length
    let len = this.msgBody.length;
    msg[idx++] = (len >> 24) & 0xff;
    msg[idx++] = (len >> 16) & 0xff;
    msg[idx++] = (len >> 8) & 0xff;
    msg[idx++] = (len >> 0) & 0xff;
    // msg body
    for (let i = 0; i < this.msgBody.length; i++) msg[idx++] = this.msgBody[i] & 0xff;
    return msg;
  };
}

function Message(id, sequence, body) {
  this.id = id;
  this.sequence = sequence;
  this.body = body;
}

/**
 * @param {*} cmdid command id
 * @param {*} handler callback function for response processing, return `non-undefined` means command ended
 */
Jensen.registerHandler = function (cmdid, handler) {
  if (typeof Jensen.handlers == 'undefined') Jensen.handlers = {};
  Jensen.handlers[cmdid] = handler;
};

Jensen.prototype.getDeviceInfo = async function (seconds) {
  return this.send(new Command(QUERY_DEVICE_INFO), seconds);
};

Jensen.prototype.getTime = async function (seconds) {
  return this.send(new Command(QUERY_DEVICE_TIME), seconds);
};

Jensen.prototype.getFileCount = async function (seconds) {
  return this.send(new Command(QUERY_FILE_COUNT), seconds);
};

Jensen.prototype.factoryReset = async function (seconds) {
  if ((this.model == 'hidock-h1' || this.model == 'hidock-h1e') && this.versionNumber < 327705) return null;
  return this.send(new Command(FACTORY_RESET), seconds);
};

Jensen.prototype.restoreFactorySettings = async function (seconds) {
  if (this.model === 'hidock-h1e' && this.versionNumber < 393476) return null;
  if (this.model === 'hidock-h1' && this.versionNumber < 327944) return null;
  return this.send(new Command(RESTORE_FACTORY_SETTINGS).body([0x01, 0x02, 0x03, 0x04]), seconds);
};

Jensen.prototype.scanDevices = async function (seconds) {
  if (this.model.indexOf('hidock-p1') == -1) return null;
  return this.send(new Command(BLUETOOTH_SCAN), seconds || 20);
};

Jensen.prototype.connectBTDevice = async function (mac, seconds) {
  if (this.model.indexOf('hidock-p1') == -1) return null;
  let marr = mac.split('-');
  if (marr.length != 6) throw new Error('invalid mac');
  let addr = [];
  for (let i = 0; i < marr.length; i++) addr[i] = parseInt(marr[i], 16);
  return this.send(new Command(BLUETOOTH_CMD).body([0x00].concat(addr)), seconds);
};

Jensen.prototype.disconnectBTDevice = async function (seconds) {
  if (this.model.indexOf('hidock-p1') == -1) return null;
  return this.send(new Command(BLUETOOTH_CMD).body([0x01]), seconds);
};

Jensen.prototype.getBluetoothStatus = async function (seconds) {
  if (this.model.indexOf('hidock-p1') == -1) return null;
  return this.send(new Command(BLUETOOTH_STATUS), seconds);
};

Jensen.prototype.listFiles = async function () {
  let tag = 'filelist-' + this.serialNumber + '-' + this.sequence();
  if (this[tag] != null) return null;

  /*
  let fc = null;
  if (typeof this.versionNumber == 'undefined' || this.versionNumber <= 327722) {
    fc = await this.getFileCount(5);
    if (fc == null) return null;
  }

  if (fc && fc.count == 0) return null;
  */

  let self = this;
  // let tag = 'data_' + new Date().getTime();
  this[tag] = [];
  this.registerHandler(QUERY_FILE_LIST, (msg, jensen) => {
    console.log('tag', tag);
    if (msg.body.length == 0) {
      console.log('remove tag1: ' + tag);
      jensen[tag] = null;
      return [];
    }
    if (jensen[tag] == null) {
      console.trace('no tag for: ' + tag);
      return 'x';
    }
    jensen[tag].push(msg.body);
    let data = [];
    let files = [];
    let fcount = -1;
    let start = 0;
    for (let i = 0; i < jensen[tag].length; i++) {
      for (let k = 0; k < jensen[tag][i].length; k++) data.push(jensen[tag][i][k]);
    }
    if ((data[0] & 0xff) == 0xff && (data[1] & 0xff) == 0xff) {
      fcount = ((data[2] & 0xff) << 24) | ((data[3] & 0xff) << 16) | ((data[4] & 0xff) << 8) | (data[5] & 0xff);
      start += 6;
    }
    let fnpad = function (v) {
      return v > 9 ? v : '0' + v;
    };
    for (let i = start; i < data.length;) {
      let len = 0;
      let fname = [];

      if (i + 4 >= data.length) break;
      let ver = data[i++] & 0xff;
      let nameLen = ((data[i++] & 0xff) << 16) | ((data[i++] & 0xff) << 8) | (data[i++] & 0xff);
      for (let k = 0; k < nameLen && i < data.length; k++) {
        let c = data[i++] & 0xff;
        if (c > 0) fname.push(String.fromCharCode(c));
      }

      // 4 + nameLen + 4 + 6 + 16
      if (i + 4 + 6 + 16 > data.length) {
        break;
      }
      let flen = ((data[i++] & 0xff) << 24) | ((data[i++] & 0xff) << 16) | ((data[i++] & 0xff) << 8) | (data[i++] & 0xff);
      i += 6;
      let sign = [];
      for (let k = 0; k < 16; k++, i++) {
        let h = (data[i] & 0xff).toString(16);
        sign.push(h.length == 1 ? '0' + h : h);
      }
      let ftime = fname.join('');
      let duration = 0;
      if (ftime.match(/^\d{14}REC\d+\.wav$/gi)) {
        ftime = ftime.replace(/^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})REC.*$/gi, '$1-$2-$3 $4:$5:$6');
        ftime = new Date(ftime);
        duration = flen / 32;
      } else if (ftime.match(/^(\d{2})?(\d{2})(\w{3})(\d{2})-(\d{2})(\d{2})(\d{2})-.*\.(hda|wav)$/gi)) {
        // 2024Mar19-110932-Rec00.hda
        ftime = ftime.replace(/^(\d{2})?(\d{2})(\w{3})(\d{2})-(\d{2})(\d{2})(\d{2})-.*\.(hda|wav)$/gi, '20$2 $3 $4 $5:$6:$7');
        ftime = new Date(ftime);
        duration = (flen / 32) * 4;
      }
      else {
        ftime = null;
      }
      if (ver == 1) {
        duration = duration * 2;
      }
      else if (ver == 2) {
        duration = (flen - 44) / 48 / 2;
      }
      else if (ver == 3) {
        duration = (flen - 44) / 48 / 2 / 2;
      }
      else if (ver == 5) {
        duration = flen / 12;
      }
      let createDate = '';
      let createTime = '';
      if (ftime) {
        createDate = ftime.getFullYear() + '/' + fnpad(ftime.getMonth() + 1) + '/' + fnpad(ftime.getDate());
        createTime = fnpad(ftime.getHours()) + ':' + fnpad(ftime.getMinutes()) + ':' + fnpad(ftime.getSeconds());
      }
      files.push({
        name: fname.join(''),
        createDate: createDate,
        createTime: createTime,
        time: ftime,
        duration: duration,
        version: ver,
        length: flen,
        signature: sign.join('')
      });
      // console.log(`Sign1: ${sign.join('')}, Sign2: ${sign2}`);
    }
    // if (fcount == -1 && (fc))
    // 如果没有判断数量的依据
    if (fcount == -1) {
      // return [];
    }

    // 如果files有重复的signature，那就持续改为不重复的为止
    // Check for duplicate signatures and regenerate if needed
    const signatureCounts = {};
    files.forEach(file => {
      signatureCounts[file.signature] = (signatureCounts[file.signature] || 0) + 1;
    });

    // Only regenerate signatures for items that have duplicates
    files.forEach(file => {
      if (signatureCounts[file.signature] > 1) {
        // Add timestamp to make signature unique
        file.signature = md5(`${file.name}${file.length}`);
      }
    });

    // 如果已经等待过长的时间了，那就直接返回好了
    let kv = tag + '-timer';
    let now = new Date().getTime();
    if (kv in jensen) {
      window.clearTimeout(jensen[kv]);
    }

    jensen[kv] = now;

    // 要怎么样触发这个回调？
    jensen[kv] = window.setTimeout(() => {
      console.log('remove tag: ' + tag);
      jensen[tag] = null;
      jensen._trigger(files.filter((f) => { return Boolean(f.time); }), QUERY_FILE_LIST);
      console.log('jensen', 'list-files', 'timed return');
    }, 1000);
  });

  return this.send(new Command(QUERY_FILE_LIST));
};

Jensen.prototype.deleteFile = async function (filename, seconds) {
  let fname = [];
  for (let i = 0; i < filename.length; i++) fname.push(filename.charCodeAt(i));
  return this.send(new Command(DELETE_FILE).body(fname), seconds);
};

Jensen.prototype.readFile = async function (filename, offset, length, seconds) {
  // 如果确定支持的版本号
  let data = [];
  data.push((offset >> 24) & 0xff);
  data.push((offset >> 16) & 0xff);
  data.push((offset >> 8) & 0xff);
  data.push((offset >> 0) & 0xff);
  data.push((length >> 24) & 0xff);
  data.push((length >> 16) & 0xff);
  data.push((length >> 8) & 0xff);
  data.push((length >> 0) & 0xff);
  for (let i = 0; i < filename.length; i++) data.push(filename.charCodeAt(i));
  return this.send(new Command(TRANSFER_FILE_PARTIAL).body(data), seconds);
}

Jensen.prototype.setTime = async function (time, seconds) {
  let str =
    time.getFullYear() +
    '-0' +
    (time.getMonth() + 1) +
    '-0' +
    time.getDate() +
    '-0' +
    time.getHours() +
    '-0' +
    time.getMinutes() +
    '-0' +
    time.getSeconds();
  str = str.replace(/(\d{4})\-0*(\d{2})\-0*(\d{2})\-0*(\d{2})\-0*(\d{2})\-0*(\d{2})/gi, '$1$2$3$4$5$6');
  return this.send(new Command(SET_DEVICE_TIME).body(this.to_bcd(str)), seconds);
};

Jensen.prototype.streaming = async function (filename, length, ondata, onprogress) {
  if (typeof length != 'number') throw new Error('parameter `length` required');
  if (length <= 0) throw new Error('parameter `length` must greater than zero');

  Logger.info('jensen', 'streaming', `file download start. filename: ${filename}, length: ${length} `);

  let fname = [];
  for (let i = 0; i < filename.length; i++) fname.push(filename.charCodeAt(i));

  let flen = 0;
  let handler = (msg) => {
    if (msg != null) {
      flen += msg.body.length || msg.body.byteLength;
      ondata(msg.body);
      Logger.info('jensen', 'streaming length', `${length} ${flen}`);
      if (flen >= length) {
        Logger.info('jensen', 'streaming', 'file download finish.');
        return 'OK';
      }
    } else {
      Logger.info('jensen', 'streaming', 'file download fail.');
      ondata('fail');
    }
  };
  this.onreceive = onprogress;
  Jensen.registerHandler(TRANSFER_FILE, handler);
  this.send(new Command(TRANSFER_FILE).body(fname));
};

Jensen.prototype.getFilePart = async function (filename, length, ondata, onprogress) {
  if (typeof length != 'number') throw new Error('parameter `length` required');
  if (length <= 0) throw new Error('parameter `length` must greater than zero');

  Logger.info('jensen', 'getFilePart', `file download start. filename: ${filename}, length: ${length} `);

  let data = [];
  data.push((length >> 24) & 0xff);
  data.push((length >> 16) & 0xff);
  data.push((length >> 8) & 0xff);
  data.push((length >> 0) & 0xff);
  for (let i = 0; i < filename.length; i++) data.push(filename.charCodeAt(i));

  let flen = 0;
  let handler = (msg) => {
    if (msg != null) {
      flen += msg.body.length || msg.body.byteLength;
      ondata(msg.body);
      Logger.info('jensen', 'getFilePart length', `${length} ${flen}`);
      if (flen >= length) {
        Logger.info('jensen', 'getFilePart', 'file download finish.');
        return 'OK';
      }
    } else {
      Logger.info('jensen', 'getFilePart', 'file download fail.');
      ondata('fail');
    }
  };
  this.onreceive = onprogress;
  Jensen.registerHandler(GET_FILE_BLOCK, handler);
  this.send(new Command(GET_FILE_BLOCK).body(data));
};

Jensen.prototype.getFile = async function (filename, length, ondata, onprogress) {
  if (typeof length != 'number') throw new Error('parameter `length` required');
  if (length <= 0) throw new Error('parameter `length` must greater than zero');

  Logger.info('jensen', 'getFile', `file download start. filename: ${filename}, length: ${length} `);

  let fname = [];
  for (let i = 0; i < filename.length; i++) fname.push(filename.charCodeAt(i));

  // let wakeLock = null;
  // function visibilitychange() {
  //   let audio = document.getElementById('test_audio');
  //   if (!audio) {
  //     audio = document.createElement('audio');
  //     audio.id = 'test_audio';
  //     audio.src = 'https://audionotes.hidock.com/test/test.aac';
  //     audio.loop = true;
  //     audio.controls = false;
  //     audio.autoplay = true;
  //     audio.muted = true;
  //     document.body.appendChild(audio);
  //   }
  //   if (document.visibilityState === 'hidden') {
  //     console.log('Page is hidden');
  //     audio.play();
  //   } else {
  //     audio.pause();
  //     console.log('Page is visible');
  //     wakeLock && wakeLock.release();
  //   }
  // }

  // if ('wakeLock' in navigator) {
  //   try {
  //     wakeLock = await navigator.wakeLock.request('screen');
  //     console.log('Wake lock activated');
  //   } catch (err) {
  //     console.error('Failed to acquire wake lock:', err);
  //   }
  // }

  // function clearEventAndTask() {
  //   const audio = document.getElementById('test_audio');
  //   audio && audio.remove();
  //   wakeLock && wakeLock.release();
  //   console.log('Clear event and task.');
  // }

  // visibilitychange();

  // document.addEventListener('visibilitychange', visibilitychange);
  let stime = new Date();
  let flen = 0;
  let handler = (msg) => {
    if (msg != null) {
      flen += msg.body.length || msg.body.byteLength;
      ondata(msg.body);
      // Logger.info('jensen', 'getFile length', `${length} ${flen}`);
      if (flen >= length) {
        // document.removeEventListener('visibilitychange', visibilitychange);
        let cost = new Date().getTime() - stime.getTime();
        Logger.info('jensen', 'getFile', 'file transfer finish, cost: ' + cost + ' ms');
        // clearEventAndTask();
        // return OK indicates all file blocks received
        return 'OK';
      }
    } else {
      // document.removeEventListener('visibilitychange', visibilitychange);
      // clearEventAndTask();
      Logger.info('jensen', 'getFile', 'file transfer fail.');
      ondata('fail');
      return 'fail';
    }
  };
  // this.onreceive = onprogress;
  this._progress_report = {};
  this.onreceive = function (recvBytes) {
    let percent = Math.floor(recvBytes / length * 100);
    let k = 't' + percent;
    if (percent % 5 == 0 && percent > 0 && percent < 100) {
      this._progress_report[k] && onprogress?.(recvBytes);
      this._progress_report[k] = true;
    }
    if (percent == 100) {
      onprogress?.(recvBytes);
    }
  }
  this.registerHandler(TRANSFER_FILE, handler);
  this.send(new Command(TRANSFER_FILE).body(fname));
};

Jensen.prototype.getAndDecode = async function () {
  // 版本兼容
}

Jensen.prototype.requestFirmwareUpgrade = async function (versionNumber, fileSize, seconds) {
  let data = [];
  data[0] = (versionNumber >> 24) & 0xff;
  data[1] = (versionNumber >> 16) & 0xff;
  data[2] = (versionNumber >> 8) & 0xff;
  data[3] = (versionNumber >> 0) & 0xff;

  data[4] = (fileSize >> 24) & 0xff;
  data[5] = (fileSize >> 16) & 0xff;
  data[6] = (fileSize >> 8) & 0xff;
  data[7] = (fileSize >> 0) & 0xff;

  return this.send(new Command(REQUEST_FIRMWARE_UPGRADE).body(data), seconds);
};

Jensen.prototype.uploadFirmware = async function (data, seconds, onprogress) {
  return this.send(new Command(FIRMWARE_UPLOAD).body(data), seconds, onprogress);
};

Jensen.prototype.beginBNC = async function (seconds) {
  return this.send(new Command(BNC_DEMO_TEST).body([0x01]), seconds);
};

Jensen.prototype.endBNC = async function (seconds) {
  return this.send(new Command(BNC_DEMO_TEST).body([0x00]), seconds);
};

Jensen.prototype.getSettings = async function (seconds) {
  if ((this.model == 'hidock-h1' || this.model == 'hidock-h1e') && this.versionNumber < 327714) {
    return { autoRecord: false, autoPlay: false };
  }
  return this.send(new Command(GET_SETTINGS), seconds);
};

Jensen.prototype.setAutoRecord = function (enable, seconds) {
  if ((this.model == 'hidock-h1' || this.model == 'hidock-h1e') && this.versionNumber < 327714) return { result: false };
  return this.send(new Command(SET_SETTINGS).body([0, 0, 0, enable ? 1 : 2]), seconds);
};

Jensen.prototype.setAutoPlay = function (enable, seconds) {
  if ((this.model == 'hidock-h1' || this.model == 'hidock-h1e') && this.versionNumber < 327714) return { result: false };
  return this.send(new Command(SET_SETTINGS).body([0, 0, 0, 0, 0, 0, 0, enable ? 1 : 2]), seconds);
};

Jensen.prototype.setNotification = function (enable, seconds) {
  if ((this.model == 'hidock-h1' || this.model == 'hidock-h1e') && this.versionNumber < 327714) return { result: false };
  return this.send(new Command(SET_SETTINGS).body([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, enable ? 1 : 2]), seconds);
};

Jensen.prototype.setBluetoothPromptPlay = function (enable, seconds) {
  // h1e 6.1.4 393476
  // h1 5.1.4 327940
  if (this.model === 'hidock-h1e' && this.versionNumber < 393476) return { result: false };
  if (this.model === 'hidock-h1' && this.versionNumber < 327940) return { result: false };
  return this.send(new Command(SET_SETTINGS).body([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, enable ? 2 : 1]), seconds);
};

Jensen.prototype.getCardInfo = function (seconds) {
  if ((this.model == 'hidock-h1' || this.model == 'hidock-h1e') && this.versionNumber < 327733) return null;
  return this.send(new Command(READ_CARD_INFO), seconds);
};

Jensen.prototype.formatCard = function (seconds) {
  if ((this.model == 'hidock-h1' || this.model == 'hidock-h1e') && this.versionNumber < 327733) return null;
  return this.send(new Command(FORMAT_CARD).body([0x01, 0x02, 0x03, 0x04]), seconds);
};

Jensen.prototype.getRecordingFile = function (seconds) {
  if ((this.model == 'hidock-h1' || this.model == 'hidock-h1e') && this.versionNumber < 327733) return null;
  return this.send(new Command(GET_RECORDING_FILE), seconds);
};

Jensen.prototype.recordTestStart = async function (type, seconds) {
  return this.send(new Command(0xf008).body([type]), seconds);
};
Jensen.prototype.recordTestEnd = async function (type, seconds) {
  return this.send(new Command(0xf009).body([type]), seconds);
};

Jensen.prototype.test = async function (seconds) {
  return this.send(new Command(DEVICE_MSG_TEST), seconds);
};

Jensen.prototype.getFileBlock = async function (filename, length, ondata) {
  if (typeof length != 'number') throw new Error('parameter `length` required');
  if (length <= 0) throw new Error('parameter `length` must greater than zero');
  if (this.fileLength > 0) return null;

  let data = [];
  data.push((length >> 24) & 0xff);
  data.push((length >> 16) & 0xff);
  data.push((length >> 8) & 0xff);
  data.push((length >> 0) & 0xff);
  for (let i = 0; i < filename.length; i++) data.push(filename.charCodeAt(i));

  this.onFileRecvHandler = ondata;
  this.fileLength = length;
  this.fileReadBytes = 0;

  return this.send(new Command(GET_FILE_BLOCK).body(data));
};
Jensen.prototype.getBatteryStatus = function (seconds) {
  if (this.model != 'hidock-p1') return null;
  return this.send(new Command(GET_BATTERY_STATUS), seconds);
}
Jensen.prototype.writeSerialNumber = async function (sn) {
  let data = [];
  for (let i = 0; i < sn.length; i++) {
    data.push(sn.charCodeAt(i));
  }
  return this.send(new Command(TEST_SN_WRITE).body(data));
};

const commonMessageParser = (msg) => {
  return { result: msg.body[0] === 0x00 ? 'success' : 'failed' };
};

Jensen.prototype.sendScheduleInfo = function (infos) {
  if (Array.isArray(infos) && infos.length) {
    let bodys = [];
    for (const info of infos) {
      let codes = new Array(34).fill(0);
      if (shortcutKeys[info.platform] && shortcutKeys[info.platform][info.os]) {
        codes = shortcutKeys[info.platform][info.os];
      }
      let startDate = new Array(8).fill(0);
      let endDate = new Array(8).fill(0);
      if (info.startDate && info.endDate) {
        startDate = sliceTime(formatTime(info.startDate));
        endDate = sliceTime(formatTime(info.endDate));
        startDate.push(0x00);
        endDate.push(0x00);
      }
      const reserved = [0x00, 0x00]; // 预留
      bodys = bodys.concat([...startDate, ...endDate, ...reserved, ...codes]);
    }
    // console.log('send schedule info', infos, bodys);
    return this.send(new Command(SCHEDULE_INFO).body(bodys));
  } else {
    const bodys = new Array(52).fill(0);
    // console.log('send schedule info', infos, bodys);
    return this.send(new Command(SCHEDULE_INFO).body(bodys));
  }
};
Jensen.prototype.transferFile = async function (filename, length, ondata, seconds) {
  let fname = [];
  for (let i = 0; i < filename.length; i++) fname.push(filename.charCodeAt(i));
  let flen = 0;
  this.registerHandler(TRANSFER_FILE, (msg) => {
    if (msg != null) {
      flen += msg.body.length || msg.body.byteLength;
      ondata(msg.body);
      // Logger.info('jensen', 'transfer-file', `${length} ${flen}`);
      if (flen >= length) {
        // Logger.info('jensen', 'transfer-file', 'file transfer finish.');
        ondata(null);
        return 'OK';
      }
    } else {
      Logger.info('jensen', 'transfer-file', 'file transfer fail.');
      ondata('fail');
    }
  });
  return this.send(new Command(TRANSFER_FILE).body(fname), seconds);
}
Jensen.prototype.getRealtimeSettings = async function () {
  return this.send(new Command(REALTIME_READ_SETTING));
};
Jensen.prototype.startRealtime = async function () {
  return this.send(new Command(REALTIME_CONTROL).body([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01]));
};
Jensen.prototype.pauseRealtime = async function () {
  return this.send(new Command(REALTIME_CONTROL).body([0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01]));
};
Jensen.prototype.stopRealtime = async function () {
  return this.send(new Command(REALTIME_CONTROL).body([0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x01]));
};
Jensen.prototype.getRealtime = async function (frames) {
  let a = (frames >> 24) & 0xff;
  let b = (frames >> 16) & 0xff;
  let c = (frames >> 8) & 0xff;
  let d = (frames >> 0) & 0xff;
  return this.send(new Command(REALTIME_TRANSFER).body([a, b, c, d]));
};
Jensen.prototype.requestToneUpdate = async function (signature, size, seconds) {
  let data = [];
  for (let i = 0; i < signature.length; i += 2) {
    let b = signature.substring(i, i + 2);
    data.push(parseInt(b, 16));
  }
  data.push((size >> 24) & 0xff);
  data.push((size >> 16) & 0xff);
  data.push((size >> 8) & 0xff);
  data.push((size >> 0) & 0xff);
  return this.send(new Command(REQUEST_TONE_UPDATE).body(data), seconds);
}
Jensen.prototype.updateTone = async function (toneFile, seconds) {
  return this.send(new Command(TONE_UPDATE).body(toneFile), seconds);
}
Jensen.prototype.requestUACUpdate = async function (signature, size, seconds) {
  let data = [];
  for (let i = 0; i < signature.length; i += 2) {
    let b = signature.substring(i, i + 2);
    data.push(parseInt(b, 16));
  }
  data.push((size >> 24) & 0xff);
  data.push((size >> 16) & 0xff);
  data.push((size >> 8) & 0xff);
  data.push((size >> 0) & 0xff);
  return this.send(new Command(REQUEST_UAC_UPDATE).body(data), seconds);
}
Jensen.prototype.updateUAC = async function (uacFile, seconds) {
  return this.send(new Command(UAC_UPDATE).body(uacFile), seconds);
}
Jensen.registerHandler(REALTIME_CONTROL, commonMessageParser);
Jensen.registerHandler(REALTIME_READ_SETTING, (msg) => {
  // console.log(msg);
  return msg;
});
Jensen.registerHandler(GET_BATTERY_STATUS, (msg) => {
  let status = msg.body[0] & 0xff;
  let percent = msg.body[1] & 0xff;
  let voltage = ((msg.body[2] & 0xff) << 24) | ((msg.body[3] & 0xff) << 16) | ((msg.body[4] & 0xff) << 8) | (msg.body[5] & 0xff);
  return {
    status: status == 0 ? 'idle' : (status == 1 ? 'charging' : 'full'),
    battery: percent,
    voltage: voltage
  };
})
Jensen.registerHandler(REALTIME_TRANSFER, (msg) => {
  let a = msg.body[0] & 0xff;
  let b = msg.body[1] & 0xff;
  let c = msg.body[2] & 0xff;
  let d = msg.body[3] & 0xff;
  return {
    rest: (a << 24) | (b << 16) | (c << 8) | d,
    data: msg.body
  };
});

Jensen.registerHandler(SET_DEVICE_TIME, commonMessageParser);
Jensen.registerHandler(BNC_DEMO_TEST, commonMessageParser);
Jensen.registerHandler(DELETE_FILE, (msg) => {
  let rst = 'failed';
  if (msg.body[0] === 0x00) rst = 'success';
  else if (msg.body[0] === 0x01) rst = 'not-exists';
  else if (msg.body[0] === 0x02) rst = 'failed';
  return { result: rst };
});
Jensen.registerHandler(QUERY_DEVICE_INFO, (msg, jensen) => {
  let vc = [],
    vn = 0;
  let sn = [];
  for (let i = 0; i < 4; i++) {
    let b = msg.body[i] & 0xff;
    if (i > 0) {
      vc.push(String(b));
    }
    vn = vn | (b << ((4 - i - 1) * 8));
  }
  for (let i = 0; i < 16; i++) {
    let chr = msg.body[i + 4];
    if (chr > 0) sn.push(String.fromCharCode(chr));
  }
  jensen.versionCode = vc.join('.');
  jensen.versionNumber = vn;
  sn = sn.join('');
  jensen.serialNumber = sn;
  return {
    versionCode: vc.join('.'),
    versionNumber: vn,
    sn: sn
  };
});
Jensen.registerHandler(QUERY_DEVICE_TIME, (msg, jensen) => {
  let time = jensen.from_bcd(
    msg.body[0] & 0xff,
    msg.body[1] & 0xff,
    msg.body[2] & 0xff,
    msg.body[3] & 0xff,
    msg.body[4] & 0xff,
    msg.body[5] & 0xff,
    msg.body[6] & 0xff
  );
  return {
    time: time === '00000000000000' ? 'unknown' : time.replace(/^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})$/gi, '$1-$2-$3 $4:$5:$6')
  };
});
Jensen.registerHandler(QUERY_FILE_COUNT, (msg) => {
  if (msg.body.length === 0) return { count: 0 };
  let c = 0;
  for (let i = 0; i < 4; i++) {
    let b = msg.body[i] & 0xff;
    c = c | (b << ((4 - i - 1) * 8));
  }
  return { count: c };
});
Jensen.registerHandler(GET_SETTINGS, (msg) => {
  let r1 = msg.body[3];
  let r2 = msg.body[7];
  let r4 = msg.body[15];
  let rst = {
    autoRecord: r1 === 1,
    autoPlay: r2 === 1,
    bluetoothTone: !(r4 === 1)
  };
  if (msg.body.length >= 12) {
    let r3 = msg.body[11] === 1;
    rst['notification'] = r3;
  }
  return rst;
});
Jensen.registerHandler(SET_SETTINGS, commonMessageParser);
Jensen.registerHandler(FACTORY_RESET, commonMessageParser);
Jensen.registerHandler(RESTORE_FACTORY_SETTINGS, commonMessageParser);
Jensen.registerHandler(REQUEST_FIRMWARE_UPGRADE, (msg) => {
  let rst = 'unknown';
  let c = msg.body[0];
  if (c === 0x00) rst = 'accepted';
  else if (c === 0x01) rst = 'wrong-version';
  else if (c === 0x02) rst = 'busy';
  else if (c === 0x03) rst = 'card-full';
  else if (c == 0x04) rst = 'card-error';
  return { result: rst };
});
Jensen.registerHandler(FIRMWARE_UPLOAD, (msg) => {
  let c = msg.body[0];
  return { result: c === 0x00 ? 'success' : 'failed' };
});
Jensen.registerHandler(READ_CARD_INFO, (msg) => {
  let i = 0;
  let used = ((msg.body[i++] & 0xff) << 24) | ((msg.body[i++] & 0xff) << 16) | ((msg.body[i++] & 0xff) << 8) | (msg.body[i++] & 0xff);
  let capacity = ((msg.body[i++] & 0xff) << 24) | ((msg.body[i++] & 0xff) << 16) | ((msg.body[i++] & 0xff) << 8) | (msg.body[i++] & 0xff);
  let status = ((msg.body[i++] & 0xff) << 24) | ((msg.body[i++] & 0xff) << 16) | ((msg.body[i++] & 0xff) << 8) | (msg.body[i++] & 0xff);

  return { used: used, capacity: capacity, status: status.toString(16) };
});
Jensen.registerHandler(TRANSFER_FILE_PARTIAL, (msg) => {
  let buf = new Uint8Array(msg.body.length);
  for (let i = 0; i < msg.body.length; i++) {
    buf[i] = msg.body[i] & 0xff;
  }
  return buf;
});
Jensen.registerHandler(FORMAT_CARD, commonMessageParser);
Jensen.registerHandler(GET_RECORDING_FILE, (msg) => {
  if (msg.body == null || msg.body.length === 0) return { recording: null };
  else {
    let fname = [];
    for (var i = 0; i < msg.body.length; i++) {
      fname.push(String.fromCharCode(msg.body[i]));
    }
    let fnpad = function (v) {
      return v > 9 ? v : '0' + v;
    };
    let ftime = fname.join('');
    if (ftime.match(/^\d{14}REC\d+\.wav$/gi)) {
      ftime = ftime.replace(/^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})REC.*$/gi, '$1-$2-$3 $4:$5:$6');
      ftime = new Date(ftime);
    } else if (ftime.match(/^(\d{2})?(\d{2})(\w{3})(\d{2})-(\d{2})(\d{2})(\d{2})-.*\.hda$/gi)) {
      // 2024Mar19-110932-Rec00.hda
      ftime = ftime.replace(/^(\d{2})?(\d{2})(\w{3})(\d{2})-(\d{2})(\d{2})(\d{2})-.*\.hda$/gi, '20$2 $3 $4 $5:$6:$7');
      ftime = new Date(ftime);
    } else {
      ftime = null;
    }
    let createDate = '';
    let createTime = '';
    if (ftime) {
      createDate = ftime.getFullYear() + '/' + fnpad(ftime.getMonth() + 1) + '/' + fnpad(ftime.getDate());
      createTime = fnpad(ftime.getHours()) + ':' + fnpad(ftime.getMinutes()) + ':' + fnpad(ftime.getSeconds());
    }
    return {
      recording: fname.join(''),
      name: fname.join(''),
      createDate: createDate,
      createTime: createTime,
      time: ftime,
      duration: 0,
      length: 0,
      signature: '0'.repeat(32)
    };
  }
});

Jensen.registerHandler(BLUETOOTH_SCAN, (msg) => {
  // console.log('bluetooth-scan', msg);
  if (msg.body.length == 0) return [];
  let nums = ((msg.body[0] & 0xff) << 8) | (msg.body[1] & 0xff);
  let devices = [];
  let decoder = new TextDecoder('UTF-8');
  for (let i = 0, k = 2; i < nums; i++) {
    let len = ((msg.body[k++] & 0xff) << 8) | (msg.body[k++] & 0xff);
    let sname = new Uint8Array(len);
    for (let f = 0; f < len; f++) {
      sname[f] = msg.body[k++] & 0xff;
    }
    let mac = [];
    for (let f = 0; f < 6; f++) {
      let m = (msg.body[k++] & 0xff).toString(16).toUpperCase();
      mac.push(m.length == 1 ? '0' + m : m);
    }
    devices.push({
      name: decoder.decode(sname),
      mac: mac.join('-')
    });
  }
  return devices;
});

Jensen.registerHandler(BLUETOOTH_STATUS, (msg) => {
  if (msg.body.length == 0) return { status: 'disconnected' };
  let status = msg.body[0];
  if (status == 1) return { status: 'disconnected' };
  let flen = ((msg.body[1] & 0xff) << 8) | (msg.body[2] & 0xff);
  let decoder = new TextDecoder('UTF-8');
  let sname = new Uint8Array(flen);
  let i = 3;
  for (let k = 0; i < msg.body.length && k < flen; i++, k++) {
    sname[k] = msg.body[i] & 0xff;
  }
  let mac = [];
  for (let k = 0; i < msg.body.length && k < 6; k++) {
    let m = msg.body[i++].toString(16).toUpperCase();
    mac.push(m.length == 1 ? '0' + m : m);
  }
  return {
    status: 'connected',
    mac: mac.join('-'),
    name: decoder.decode(sname),
    a2dp: (msg.body[i++] & 0xff) == 1,
    hfp: (msg.body[i++] & 0xff) == 1,
    avrcp: (msg.body[i++] & 0xff) == 1,
    battery: parseInt(((msg.body[i++] & 0xff) / 255) * 100)
  };
});

Jensen.registerHandler(RECORD_TEST_START, commonMessageParser);
Jensen.registerHandler(RECORD_TEST_END, commonMessageParser);
Jensen.registerHandler(DEVICE_MSG_TEST, commonMessageParser);
Jensen.registerHandler(GET_FILE_BLOCK, commonMessageParser);
Jensen.registerHandler(TEST_SN_WRITE, commonMessageParser);
Jensen.registerHandler(SCHEDULE_INFO, commonMessageParser);
Jensen.registerHandler(BLUETOOTH_CMD, commonMessageParser);
Jensen.registerHandler(REQUEST_TONE_UPDATE, (msg) => {
  let rst = msg.body[0];
  let txt = 'success';
  if (rst == 0x01) txt = 'length-mismatch';
  else if (rst == 0x02) txt = 'busy';
  else if (rst == 0x03) txt = 'card-full';
  else if (rst == 0x04) txt = 'card-error';
  else txt = String(rst);
  return { code: rst, result: txt };
});
Jensen.registerHandler(TONE_UPDATE, commonMessageParser);
Jensen.registerHandler(REQUEST_UAC_UPDATE, (msg) => {
  let rst = msg.body[0];
  let txt = 'success';
  if (rst == 0x01) txt = 'length-mismatch';
  else if (rst == 0x02) txt = 'busy';
  else if (rst == 0x03) txt = 'card-full';
  else if (rst == 0x04) txt = 'card-error';
  else txt = String(rst);
  return { code: rst, result: txt };
});
Jensen.registerHandler(UAC_UPDATE, commonMessageParser);
export { Jensen };