const spawn = require("child_process").spawn;
const NALseparator = Buffer.from([0, 0, 0, 1]);
const { WebSocketServer } = require("ws");
const status = require("./status");
const { asyncCommand } = require("./unit");
const sessionManager = require("./session");

exports.NALseparator = NALseparator;

module.exports = class Camera {
  constructor(options) {
    this.options = options;
    const { cameraIndex, cardType, name, deviceSize, server, devPath, label } =
      options;

    this.rtcDataChannel = [];
    this.formatList = [];
    this.formatListPromise = getCameraFormats(devPath)
      .then((v) => {
        this.formatList = v.filter(({ format }) =>
          ["yuyv422", "mjpeg", "h264", "grey"].includes(format)
        );
        logger.info(`${label} 格式列表`, this.formatList);
        return this.formatList;
      })
      .catch((error) => {
        logger.error(`${label} 获取格式列表失败: ${error.message}`);
        this.formatList = [];
        return this.formatList;
      });

    if (deviceSize.width - 0 > 640) {
      deviceSize.height = ((deviceSize.height - 0) / deviceSize.width) * 640;
      deviceSize.width = 640;
    }

    this.label = label;
    this.maxSizeWidth =
      deviceSize.width > status.config.cameraMaxWidth
        ? status.config.cameraMaxWidth
        : deviceSize.width;
    const path = `/video${cameraIndex}`;
    logger.info(`Camera ${this.label} websocker server starting`, path);
    const wss = (this.wss = new WebSocketServer(
      {
        noServer: true,
        path,
      },
      () => {
        logger.info(`Camera ${this.label} websocker server started`, path);
      }
    ));

    server.on("upgrade", (request, socket, head) => {
      // todo 权限
      if (request.url === path)
        wss.handleUpgrade(request, socket, head, (ws) => {
          wss.emit("connection", ws, request);
        });
    });

    wss.on("connection", async (socket) => {
      logger.info(`客户端已连接 Camera ${this.label}`);

      socket.on("message", (data) => {
        this.messageHandle(data, "ws", socket);
      });

      socket.on("close", () => {
        this.onClose();
      });
      socket.send(JSON.stringify({ acrion: "ready" }));
    });

    wss.on("error", (err) => {
      logger.error(err);
    });
  }

  async open({ width = 400, inputFormatIndex, fps }) {
    logger.info("inputFormatIndex:" + inputFormatIndex);
    logger.info("fps:" + fps);
    logger.info("width:" + width);
    const { deviceSize, devPath } = this.options;

    if (!this.formatList || this.formatList.length === 0) {
      await this.formatListPromise;
    }
    if (!this.formatList || this.formatList.length === 0) {
      logger.warn(`Camera ${this.label} 无可用格式，跳过打开摄像头`);
      this.broadcast("stream_active", false);
      return;
    }
    width = Math.floor(width);
    if (width > this.maxSizeWidth) {
      width = this.maxSizeWidth;
    }

    let height = (width / deviceSize.width) * deviceSize.height;

    /** 编码器要求分辨率为 x 的倍数 */
    const x = 32;
    width = Math.ceil(width / x) * x;
    height = Math.ceil(height / x) * x;

    if (this.streamer) {
      logger.info(`Camera ${this.label} ffmpeg streamer already open`);
      this.close();
    }

    logger.info(`Camera ${this.label} , 输出分辨率:${width}x${height}`);

    this.currentSize = { width, height };

    this.broadcast("open", { inputFormatIndex, fps });
    status.saveConfig({ [devPath]: { inputFormatIndex, fps } });
    this.broadcast("initalize", {
      size: { width, height },
      cameraName: this.label,
    });
    this.broadcast("stream_active", true);

    if (
      inputFormatIndex === undefined ||
      inputFormatIndex < 0 ||
      inputFormatIndex >= this.formatList.length
    ) {
      const index = this.formatList.findIndex(
        ({ format }) => format === "mjpeg"
      );
      inputFormatIndex = index < 0 ? 0 : index;
    }

    this.sps = null;
    this.pps = null;

    this.nalMode = null;
    this.nalBuffer = Buffer.alloc(0);
    this.nalLengthSize = 4;
    this.nalDebug = process.env.NRC_NAL_DEBUG === "1";
    this.nalModeLogged = false;
    this.nalSeenSps = false;
    this.nalSeenPps = false;
    this.nalSeenIdr = false;
    this.auBuffer = [];
    this.hasAud = false;

    const findStartCode = (buf, from = 0) => {
      for (let i = from; i < buf.length - 3; i++) {
        if (buf[i] === 0x00 && buf[i + 1] === 0x00) {
          if (buf[i + 2] === 0x01) return { index: i, length: 3 };
          if (buf[i + 2] === 0x00 && buf[i + 3] === 0x01)
            return { index: i, length: 4 };
        }
      }
      return null;
    };

    const flushAccessUnit = () => {
      if (!this.auBuffer || this.auBuffer.length === 0) return;
      const hasIdr = this.auBuffer.some((nal) => (nal[0] & 0x1f) === 5);
      const hasSps = this.auBuffer.some((nal) => (nal[0] & 0x1f) === 7);
      const hasPps = this.auBuffer.some((nal) => (nal[0] & 0x1f) === 8);
      const hasAud = this.auBuffer.some((nal) => (nal[0] & 0x1f) === 9);

      const units = [];
      if (hasIdr && this.sps && !hasSps) units.push(this.sps);
      if (hasIdr && this.pps && !hasPps) units.push(this.pps);
      if (!hasAud) {
        units.push(...this.auBuffer);
      } else {
        this.auBuffer.forEach((nal) => {
          const type = nal[0] & 0x1f;
          if ((type === 7 && hasIdr && this.sps) || (type === 8 && hasIdr && this.pps)) {
            return;
          }
          units.push(nal);
        });
      }

      const out = Buffer.concat(units.map((nal) => Buffer.concat([NALseparator, nal])));
      this.broadcastStream(out);
      this.auBuffer = [];
    };

    const removeEmulationPrevention = (buf) => {
      const out = [];
      for (let i = 0; i < buf.length; i++) {
        if (i > 1 && buf[i] === 0x03 && buf[i - 1] === 0x00 && buf[i - 2] === 0x00) {
          continue;
        }
        out.push(buf[i]);
      }
      return Buffer.from(out);
    };

    const readUE = (reader) => {
      let zeros = 0;
      while (reader.readBit() === 0) {
        zeros++;
        if (zeros > 31) return null;
      }
      let value = 1 << zeros;
      for (let i = 0; i < zeros; i++) {
        value |= reader.readBit() << (zeros - 1 - i);
      }
      return value - 1;
    };

    const getFirstMbInSlice = (nal) => {
      if (!nal || nal.length < 2) return null;
      const rbsp = removeEmulationPrevention(nal.slice(1));
      let bitPos = 0;
      const reader = {
        readBit() {
          if (bitPos >= rbsp.length * 8) return 1;
          const byte = rbsp[bitPos >> 3];
          const bit = (byte >> (7 - (bitPos & 7))) & 1;
          bitPos++;
          return bit;
        },
      };
      return readUE(reader);
    };

    const pushFrame = (frame) => {
      if (!frame || frame.length === 0) return;
      const nalType = frame[0] & 0x1f;
      if (nalType === 7) this.sps = frame;
      if (nalType === 8) this.pps = frame;
      if (this.nalDebug) {
        if (nalType === 7 && !this.nalSeenSps) {
          this.nalSeenSps = true;
          logger.info(`Camera ${this.label} NAL: SPS`);
        }
        if (nalType === 8 && !this.nalSeenPps) {
          this.nalSeenPps = true;
          logger.info(`Camera ${this.label} NAL: PPS`);
        }
        if (nalType === 5 && !this.nalSeenIdr) {
          this.nalSeenIdr = true;
          logger.info(`Camera ${this.label} NAL: IDR`);
        }
      }

      if (!this.sps || !this.pps) {
        if (nalType !== 7 && nalType !== 8) return;
      }

      if (nalType === 9) {
        this.hasAud = true;
        flushAccessUnit();
      }

      if (nalType === 1 || nalType === 5) {
        const firstMb = getFirstMbInSlice(frame);
        if (firstMb === 0 && this.auBuffer.length > 0) {
          flushAccessUnit();
        }
      }

      this.auBuffer.push(frame);
    };

    const processAnnexB = () => {
      const starts = [];
      let pos = 0;
      while (true) {
        const sc = findStartCode(this.nalBuffer, pos);
        if (!sc) break;
        starts.push(sc);
        pos = sc.index + sc.length;
      }
      if (starts.length === 0) {
        if (this.nalBuffer.length > 4 * 1024 * 1024) {
          this.nalBuffer = this.nalBuffer.slice(-1024);
        }
        return;
      }
      for (let i = 0; i < starts.length - 1; i++) {
        const start = starts[i];
        const end = starts[i + 1].index;
        const frame = this.nalBuffer.slice(start.index + start.length, end);
        pushFrame(frame);
      }
      this.nalBuffer = this.nalBuffer.slice(starts[starts.length - 1].index);
    };

    const processAvcc = () => {
      const maxNal = 2 * 1024 * 1024;
      const lenSize = this.nalLengthSize === 2 ? 2 : 4;
      while (this.nalBuffer.length >= lenSize) {
        const nalSize =
          lenSize === 2
            ? this.nalBuffer.readUInt16BE(0)
            : this.nalBuffer.readUInt32BE(0);
        if (nalSize <= 0 || nalSize > maxNal) {
          this.nalMode = null;
          this.nalBuffer = Buffer.alloc(0);
          return;
        }
        if (this.nalBuffer.length < lenSize + nalSize) return;
        const frame = this.nalBuffer.slice(lenSize, lenSize + nalSize);
        this.nalBuffer = this.nalBuffer.slice(lenSize + nalSize);
        pushFrame(frame);
      }
    };

    this.streamer = ffmpeg(
      { width, height },
      this.options.devPath,
      this.formatList[inputFormatIndex],
      fps
    );

    this.streamer.stdout.on("data", (chunk) => {
      if (!chunk || chunk.length === 0) return;
      this.nalBuffer = Buffer.concat([this.nalBuffer, chunk]);
      if (!this.nalMode) {
        const sc = findStartCode(this.nalBuffer, 0);
        if (sc) {
          this.nalMode = "annexb";
          if (this.nalDebug && !this.nalModeLogged) {
            this.nalModeLogged = true;
            logger.info(`Camera ${this.label} NAL mode: annexb`);
          }
        } else if (this.nalBuffer.length >= 4) {
          const maxNal = 2 * 1024 * 1024;
          const nalSize4 = this.nalBuffer.readUInt32BE(0);
          const nalSize2 = this.nalBuffer.readUInt16BE(0);
          if (nalSize4 > 0 && nalSize4 < maxNal) {
            this.nalMode = "avcc";
            this.nalLengthSize = 4;
          } else if (nalSize2 > 0 && nalSize2 < maxNal) {
            this.nalMode = "avcc";
            this.nalLengthSize = 2;
          }
          if (this.nalMode === "avcc" && this.nalDebug && !this.nalModeLogged) {
            this.nalModeLogged = true;
            logger.info(
              `Camera ${this.label} NAL mode: avcc(len=${this.nalLengthSize})`
            );
          }
        }
      }
      if (this.nalMode === "annexb") processAnnexB();
      else if (this.nalMode === "avcc") processAvcc();
    });

    this.streamer.stdout.on("end", () => {
      flushAccessUnit();
      this.broadcast("stream_active", false);
    });
  }

  messageHandle(data, type, transport) {
    const { action, payload } = JSON.parse(data);
    const config = status.config[this.options.devPath] || {};
    logger.info(
      `Camera Server ${this.label} 收到 ${type} 的 message:${JSON.stringify(
        payload
      )}`
    );
    switch (action) {
      case "open-request":
        if (!transport.user) return;
        let {
          size,
          inputFormatIndex,
          fps = 30,
        } = {
          ...config,
          ...payload,
        };
        if (!this.formatList || this.formatList.length === 0) {
          inputFormatIndex = 0;
        } else if (inputFormatIndex === undefined) {
          const index = this.formatList.findIndex(
            ({ format }) => format === "mjpeg"
          );
          inputFormatIndex = index < 0 ? 0 : index;
        }
        transport.cameraEnabled = true;
        this.open({ width: size && size.width, inputFormatIndex, fps });

        break;
      case "get-info":
        const user = sessionManager.list.find(
          (i) => i.id === payload.sessionId
        );
        transport.user = user;
        if (!transport.user) return;
        transport.send(
          JSON.stringify({
            action: "info",
            payload: {
              size: this.options.deviceSize,
              cameraName: this.label,
              formatList: this.formatList || [],
              ...config,
            },
          })
        );
      case "close":
        transport.cameraEnabled = false;
        this.onClose();
      default:
    }
  }

  onClose() {
    logger.info(`Camera Server ${this.label} onClose`);
    const rtcCount = this.rtcDataChannel.reduce(
      (p, { cameraEnabled }) => p + (cameraEnabled ? 1 : 0),
      0
    );
    const wsClientCount = this.wss.clients.size;
    logger.info(
      `Camera Server wsClientCount ${wsClientCount}, rtcCount: ${rtcCount}`
    );
    if (rtcCount + wsClientCount === 0) {
      this.close();
    }
  }

  close() {
    if (this.streamer) {
      logger.info(`Camera Server ${this.label}: ffmpeg streamer killing`);
      this.streamer.kill();
      this.streamer = undefined;
      this.currentSize = undefined;
    }
  }

  sendBinary(socket, frame) {
    if (socket.buzy) return;
    socket.buzy = true;
    socket.buzy = false;

    socket.send(frame, { binary: true }, function ack() {
      socket.buzy = false;
    });
  }

  broadcast(action, payload) {
    logger.info(
      `Camera ${
        this.label
      } broadcase action:${action}, payload:${JSON.stringify(payload)}`
    );
    this.wss.clients.forEach((socket) =>
      socket.send(JSON.stringify({ action, payload }))
    );
    this.rtcDataChannel.forEach((channel) => {
      if (channel.user && channel.readyState === "open")
        channel.send(JSON.stringify({ action, payload }));
    });
  }

  broadcastStream(data) {
    this.wss.clients.forEach((socket) => {
      socket.cameraEnabled && this.sendBinary(socket, data);
    });
    this.rtcDataChannel.forEach((channel) => {
      if (
        channel.user &&
        channel.cameraEnabled &&
        channel.readyState === "open"
      )
        channel.send(data);
    });
  }

  pushRTCDataChannel(channel) {
    logger.info(`Camera Server ${channel.label} 切换到 Webrtc Data Channel`);
    this.rtcDataChannel.push(channel);
    channel.addEventListener("message", ({ data }) => {
      this.messageHandle(data, "rtc", channel);
    });
    channel.send(JSON.stringify({ acrion: "ready" }));
  }
  removeRTCDataChannel(channel) {
    logger.info(`Camera Server ${channel.label} 切换到 Websocket`);
    this.rtcDataChannel.filter((i) => i !== channel);
  }
};

module.exports.getCameraList = async function () {
  try {
    const devList = [];
    return new Promise((resolve) => {
      const v4l = spawn("v4l2-ctl", ["--list-devices"]);
      v4l.stdout.on("data", (data) => {
        data
          .toString()
          .split("\n")
          .forEach((line) => {
            if (/\/dev\/video[0-9]$/.test(line)) {
              devList.push({ dev: line.trim() });
            }
          });
      });
      v4l.on("exit", async () => {
        for (let index = 0; index < devList.length; index++) {
          const item = devList[index];
          let outText = await asyncCommand(
            `v4l2-ctl --device=${item.dev} --all`
          );
          const name = /Driver name\s*\:\s*(.+)/i.exec(outText)[1];
          const cardType = /Card type\s*\:\s*(.+)/i.exec(outText)[1];
          outText = await asyncCommand(
            `v4l2-ctl --device=${item.dev} --list-formats-ext`
          );
          const sizeR = /(\d+x\d+)/g;
          let size;
          let match;
          while ((match = sizeR.exec(outText)) !== null) {
            let [width, height] = match[0].split("x");
            width = width - 0;
            height = height - 0;
            if (size) {
              if (size.width < width) {
                size = { width, height };
              }
            } else {
              size = { width, height };
            }
          }
          item.name = name;
          item.cardType = cardType;
          item.size = size;
          item.label = `${name}(${cardType})`;
        }

        devList.forEach(({ label, cardType, dev, size }) => {
          if (size) {
            logger.info(
              `  ${label} \n 最大分辨率: ${size.width}x${size.height}`
            );
          } else {
            logger.info(`  ${label}  \n 无法获取分辨率，已过滤`);
          }
        });
        resolve(devList.filter((i) => i.size));
      });
    });
  } catch (e) {
    logger.error(e);
  }
};

const getCameraFormats = async function (devPath) {
  const result = await asyncCommand(
    `ffmpeg -f v4l2 -list_formats all -i ${devPath}`,
    "stderr"
  );
  const regexp =
    /\[video4linux2,v4l2 \@ ([\s\S]+?)\] ([\S]+) *?\:\s*([\S]+) \: +[\s\S]+?: ([\s\S]+?)\n/g;
  const list = [];
  while ((match = regexp.exec(result)) != null) {
    const [string, id, compressed, format, size = ""] = match;
    (size.match(/\d+x\d+/g) || ["640x480"]).forEach((size) => {
      list.push({ id, format, size });
    });
  }
  return list;
};

const ffmpeg = function (outSize, input, inputformat, fps = 30) {
  logger.info(`${input} input format`, inputformat);
  logger.info(`${input} input fps`, fps);

  // 支持硬编与软编切换
  let videoEncoder = process.env.NRC_VIDEO_ENCODER || "libx264";
  videoEncoder = String(videoEncoder).trim() || "libx264";
  const isHwEncoder = /v4l2m2m/i.test(videoEncoder);
  if (isHwEncoder) {
    logger.info(`当前使用硬件编码 ${videoEncoder}`);
  } else {
    logger.warn(`当前使用软编码 ${videoEncoder}`);
  }

  const hwPixFmt = process.env.NRC_HW_PIX_FMT || "nv12";
  const swPixFmt = process.env.NRC_SW_PIX_FMT || "yuv420p";
  const hwProfileEnv = process.env.NRC_HW_PROFILE;
  const hwProfile =
    hwProfileEnv === undefined ? "66" : String(hwProfileEnv).trim();
  const hwBframesEnv = process.env.NRC_HW_BFRAMES;
  const hwBframes =
    hwBframesEnv === undefined ? "0" : String(hwBframesEnv).trim();
  const hwGopEnv = process.env.NRC_HW_GOP;
  const hwGop = hwGopEnv === undefined ? "30" : String(hwGopEnv).trim();
  const forceKeyFramesEnv = process.env.NRC_HW_FORCE_KEYFRAMES;
  const forceKeyFrames =
    forceKeyFramesEnv === undefined
      ? "expr:gte(t,n_forced*1)"
      : String(forceKeyFramesEnv).trim();
  const h264BsfEnv = process.env.NRC_H264_BSF;
  const h264Bsf =
    h264BsfEnv === undefined
      ? "h264_metadata=aud=1,dump_extra"
      : String(h264BsfEnv).trim();
  const swPreset = (process.env.NRC_SW_PRESET || "ultrafast").trim();
  const swTune = (process.env.NRC_SW_TUNE || "zerolatency").trim();
  const swProfile = (process.env.NRC_SW_PROFILE || "baseline").trim();

  const encoderArgs = [
    "-c:v",
    videoEncoder,
    "-pix_fmt",
    isHwEncoder ? hwPixFmt : swPixFmt,
    "-b:v",
    "1000k",
    "-g",
    hwGop,
  ];

  if (isHwEncoder) {
    if (hwProfile && !["off", "none"].includes(hwProfile.toLowerCase())) {
      encoderArgs.push("-profile:v", hwProfile);
    }
    if (hwBframes !== "") {
      encoderArgs.push("-bf", hwBframes);
    }
  } else {
    if (swProfile && !["off", "none"].includes(swProfile.toLowerCase())) {
      encoderArgs.push("-profile:v", swProfile);
    }
    if (swPreset && !["off", "none"].includes(swPreset.toLowerCase())) {
      encoderArgs.push("-preset", swPreset);
    }
    if (swTune && !["off", "none"].includes(swTune.toLowerCase())) {
      encoderArgs.push("-tune", swTune);
    }
  }

  const scaleFilter = `scale=${outSize.width}:${outSize.height}`;
  const videoFilter = `${scaleFilter},format=${
    isHwEncoder ? hwPixFmt : swPixFmt
  }`;

  const args = [
    "-f",
    "video4linux2",
    "-input_format",
    inputformat.format,
    "-s",
    inputformat.size,
    "-r",
    fps,
    "-i",
    input,
    "-vf",
    videoFilter,
    ...encoderArgs,
    "-r",
    fps,
  ];

  if (forceKeyFrames && !["off", "none"].includes(forceKeyFrames.toLowerCase())) {
    args.push("-force_key_frames", forceKeyFrames);
  }

  if (h264Bsf && !["off", "none"].includes(h264Bsf.toLowerCase())) {
    args.push("-bsf:v", h264Bsf);
  }

  args.push("-f", "h264", "-");

  if (process.env.NRC_FFMPEG_LOG === "1") {
    logger.info(`ffmpeg args: ${args.join(" ")}`);
  }

  const streamer = spawn("ffmpeg", args);
  const stderrLines = [];
  const maxStderrLines = 30;

  streamer.on("close", (code, signal) => {
    logger.info("Streamer ffmpeg streamer close", { code, signal });
    if (code && stderrLines.length) {
      logger.warn(
        `ffmpeg 退出(${code}) ${signal || ""}，最近日志：\n${stderrLines.join(
          "\n"
        )}`
      );
    }
  });

  streamer.stderr.on("data", (data) => {
    const text = data.toString();
    if (process.env.NRC_FFMPEG_LOG === "1") {
      logger.info(`ffmpeg stderr ${input}: ${text}`);
    }
    text
      .split("\n")
      .filter(Boolean)
      .forEach((line) => {
        stderrLines.push(line);
        if (stderrLines.length > maxStderrLines) stderrLines.shift();
      });
  });

  streamer.stdout.on("data", (data) => {
    if (process.env.NRC_FFMPEG_LOG === "1") {
      logger.info(`ffmpeg stdout ${input}: ${data.toString()}`);
    }
  });

  return streamer;
};
