/*
 *			GPAC - Multimedia Framework C SDK
 *
 *			Authors: Deniz Ugur
 *			Copyright (c) Motion Spell 2025
 *					All rights reserved
 *
 *  This file is part of GPAC / Text Generator filter
 *
 *  GPAC is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU Lesser General Public License as published by
 *  the Free Software Foundation; either version 2, or (at your option)
 *  any later version.
 *
 *  GPAC is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU Lesser General Public License for more details.
 *
 *  You should have received a copy of the GNU Lesser General Public
 *  License along with this library; see the file COPYING.  If not, write to
 *  the Free Software Foundation, 675 Mass Ave, Cambridge, MA 02139, USA.
 *
 */

import { Sys as sys } from "gpaccore";
import { File as File } from "gpaccore";

filter.pids = [];

filter.set_name("txtgen");
filter.set_class_hint(GF_FS_CLASS_MM_IO);
filter.set_desc("Text Generator");
filter.set_version("1.0");
filter.set_author("GPAC Team");
filter.set_help(
  "This filter generates text streams based on the provided [-src]() file. By default, the filter uses a lorem ipsum text file\n" +
    "The [-type]() parameter sets the text generation mode. If set to 'txt', the filter will generate text based on the source file\n" +
    "If set to 'utc', the filter will generate text based on the current UTC time. If set to 'ntp', the filter will generate text based on the current NTP time\n" +
    "When the [-unit]() is set to 'w', the filter will generate text based on words. When set to 'l', the filter will generate text based on lines\n" +
    "The [-fdur]() parameter sets the frame duration of the text stream. " +
    "Total duration of the text stream is set by the [-dur]() parameter. If set to 0/0, the text stream will be infinite\n" +
    "The [-rollup]() parameter enables roll-up mode up to the specified number of lines. In roll-up mode, the filter will accumulate text until the specified number of lines is reached.\n" +
    "When the number of lines is reached, the filter will remove the first line and continue accumulating text\n" +
    "You would use [-rollup]() in combination with [-unit]() set to 'l' to create a roll-up subtitle effect. Or set [-unit]() to 'w' to create a roll-up text effect.\n" +
    "The [-lmax]() parameter sets the maximum number of characters in a line. If the line in the source file is longer than this, the excess text will be wrapped. 0 means no limit\n" +
    "When [-rt]() is set to true, the filter will generate text in real-time. If set to false, the filter will generate text as fast as possible"
);

filter.set_arg({
  name: "src",
  desc: "source of text. If not set, the filter will use lorem ipsum text",
  type: GF_PROP_STRING,
  def: filter.jspath + "lipsum.txt",
});
filter.set_arg({
  name: "type",
  desc: "type of text to generate\n- txt: plain text (uses src option)\n- utc: UTC time\n- ntp: NTP time",
  type: GF_PROP_UINT,
  def: "txt",
  minmax_enum: "txt|utc|ntp",
});
filter.set_arg({
  name: "unit",
  desc: "minimum unit of text from the source\n- w: word\n- l: line",
  type: GF_PROP_UINT,
  def: "l",
  minmax_enum: "w|l",
});
filter.set_arg({
  name: "fdur",
  desc: "duration of each frame",
  type: GF_PROP_FRACTION,
  def: "1/1",
});
filter.set_arg({
  name: "lmax",
  desc: "maximum number of characters in a line. If the line in the source file is longer than this, the excess text will be wrapped. 0 means no limit",
  type: GF_PROP_UINT,
  def: 32,
});
filter.set_arg({
  name: "dur",
  desc: "duration of the text stream",
  type: GF_PROP_FRACTION,
  def: "0/0",
});
filter.set_arg({
  name: "rollup",
  desc: "enable roll-up mode up to the specified number of lines",
  type: GF_PROP_UINT,
  def: 0,
});
filter.set_arg({
  name: "lock",
  desc: "lock timing to text generation",
  type: GF_PROP_BOOL,
  def: "false",
});
filter.set_arg({
  name: "rt",
  desc: "real-time mode",
  type: GF_PROP_BOOL,
  def: "true",
});

let text_cts = 0;
let text_pid = null;

let text = [];
let text_idx = 0;
let text_playing = false;
let dyn_rp_text = "";

let start_date = 0;
let utc_init = 0;
let ntp_init = 0;
let last_utc = 0;

filter.frame_pending = 0;

filter.initialize = function () {
  this.set_cap({ id: "StreamType", value: "Text", output: true });
  this.set_cap({ id: "CodecID", value: "txt", output: true });

  let gpac_help = sys.get_opt("temp", "gpac-help");
  let gpac_doc = sys.get_opt("temp", "gendoc") == "yes" ? true : false;
  if (gpac_help || gpac_doc) return;

  if (filter.fdur.n <= 0) {
    print(GF_LOG_ERROR, "Unit duration cannot be 0 or negative");
    return GF_BAD_PARAM;
  } else if (filter.dur.n < 0) {
    print(GF_LOG_ERROR, "Stream duration cannot be negative");
    return GF_BAD_PARAM;
  }

  //setup text
  text_pid = this.new_pid();
  text_pid.set_prop("StreamType", "Text");
  text_pid.set_prop("CodecID", "txt");
  text_pid.set_prop("Unframed", true);
  text_pid.set_prop("Cached", true);
  text_pid.set_prop("Timescale", 1000);
  text_pid.name = "text";
  text_pid.set_prop("ID", 1);
  text_pid.set_prop("Bitrate", 1);

  //using UTC or NTP
  if (filter.type != 0) {
    if (filter.unit == 1) {
      print(
        GF_LOG_DEBUG,
        "UTC/NTP mode does not support line unit, switching to word unit"
      );
      filter.unit = 0; //since "line" is the default, don't warn
    }
    return;
  }

  //load lipsum text
  let file = new File(filter.src, "r");
  let size = file.size;
  while (!file.eof) {
    let line = file.gets();
    if (line) {
      if (line.trim().length == 0) {
        if (filter.unit == 1) text.push(null);
        continue;
      }

      if (filter.unit == 0) {
        let words = line.trim().split(" ");
        for (let i = 0; i < words.length; i++) {
          text.push(words[i]);
        }
      } else if (filter.unit == 1) {
        text.push(line.trim());
      }
    }
  }
  text.push(null);
  file.close();

  if (filter.lmax > 0) {
    let newText = [];
    let prevWarped = false;
    for (let i = 0; i < text.length; i++) {
      let cur = text[i];
      if (cur) {
        let words = cur.split(" ");
        let line = "";
        if (prevWarped && newText.length) {
          if (newText[newText.length - 1] != null) {
            line = newText.pop();
          }
        }
        for (let j = 0; j < words.length; j++) {
          if (line.length + words[j].length > filter.lmax) {
            if (!line.length) {
              print(
                GF_LOG_WARNING,
                "Encountered a word longer than lmax (" +
                  words[j].length +
                  " > " +
                  filter.lmax +
                  "), adjusting lmax"
              );
              filter.lmax = words[j].length + 1;
            } else {
              newText.push(line);
              line = "";
              prevWarped = true;
            }
          }
          if (line.length) line += " ";
          line += words[j];
        }
        newText.push(line);
      } else {
        newText.push(null);
      }
    }
    text = newText;
  }

  //rollup algorithm
  if (filter.rollup > 0) {
    let newText = [];
    let accumulatedText = "";
    let lineCount = 0;
    for (let i = 0; i < text.length; i++) {
      let cur = text[i];

      if (cur) {
        if (filter.unit == 0) {
          if (accumulatedText.length > 0) accumulatedText += " ";
          else lineCount++;
        }
        let lastLine = accumulatedText.lastIndexOf("\n") + 1;
        let lastLineLength = accumulatedText.length - lastLine;
        if (lastLineLength + cur.length > filter.lmax) {
          lineCount++;
          accumulatedText += "\n";
        }
        accumulatedText += cur;
      }
      if (cur == null || filter.unit == 1) {
        lineCount++;
        accumulatedText += "\n";
      }

      while (lineCount > filter.rollup) {
        //remove first line
        let idx = accumulatedText.indexOf("\n");
        accumulatedText = accumulatedText.substring(idx + 1);
        lineCount--;
      }

      //clear leading newlines
      while (accumulatedText.startsWith("\n")) {
        accumulatedText = accumulatedText.substring(1);
        lineCount--;
      }

      //clear trailing newlines
      while (accumulatedText.endsWith("\n\n")) {
        accumulatedText = accumulatedText.substring(
          0,
          accumulatedText.length - 1
        );
        lineCount--;
      }

      if (text.length - 1 != i) {
        newText.push(accumulatedText.trim());
      }
    }
    newText.push(null);
    text = newText;
  }

  //subtitle at every N ms
  let bitrate = Math.floor(
    ((size / text.length) * 8) / (filter.fdur.n / filter.fdur.d)
  );
  text_pid.set_prop("Bitrate", bitrate);
};

filter.process_event = function (pid, evt) {
  if (evt.type == GF_FEVT_STOP) {
    if (pid === text_pid) text_playing = false;
  } else if (evt.type == GF_FEVT_PLAY) {
    if (pid === text_pid) text_playing = true;
    filter.reschedule();
  }
};

function getTimes() {
  let date = new Date();
  let utc, ntp;
  if (filter.lock) {
    let elapsed_us = 1000 * text_cts;
    let elapsed_ms = Math.floor(elapsed_us / 1000);
    date.setTime(start_date + elapsed_ms);
    utc = utc_init + elapsed_ms;
    ntp = sys.ntp_shift(ntp_init, elapsed_us);
  } else {
    utc = sys.get_utc();
    ntp = sys.get_ntp();
  }
  return [utc, ntp];
}

filter.process = function () {
  if (!text_playing) return GF_EOS;
  if (!text_pid || text_pid.would_block) return GF_OK;

  //init times
  if (!start_date) {
    start_date = new Date().getTime();
    utc_init = sys.get_utc();
    ntp_init = sys.get_ntp();
  }

  //regulate text generation
  let interval_ms = Math.floor((1000 * filter.fdur.n) / filter.fdur.d);
  if (filter.rt) {
    if (last_utc && sys.get_utc() - last_utc < interval_ms) return GF_OK;
    last_utc = sys.get_utc();
    filter.reschedule(interval_ms);
  }

  //decide on unit
  let unit;
  if (filter.type == 0) {
    unit = text[text_idx++ % text.length];
  } else {
    let times = getTimes();
    if (filter.type == 1) {
      unit = new Date(times[0]).toUTCString();
    } else {
      unit = times[1].n + "." + times[1].d.toString(16);
    }
  }

  //handle gaps
  if (filter.rollup && filter.type > 0) {
    let lineCount = dyn_rp_text.split("\n").length;
    if (lineCount >= filter.rollup) {
      dyn_rp_text = dyn_rp_text.substring(dyn_rp_text.indexOf("\n") + 1);
    }
    if (dyn_rp_text.length) dyn_rp_text += "\n";
    dyn_rp_text += unit;
  } else if (unit == null) {
    text_cts += interval_ms;
    return GF_OK;
  }

  //prepare packet
  let text_to_send = dyn_rp_text.length ? dyn_rp_text : unit;
  let pck = text_pid.new_packet(text_to_send.length);
  let farray = new Uint8Array(pck.data);
  for (let i = 0; i < text_to_send.length; i++) {
    farray[i] = text_to_send.charCodeAt(i);
  }
  let dur = interval_ms;
  pck.cts = text_cts;
  pck.dur = dur;
  pck.sap = GF_FILTER_SAP_1;

  //update text cts and reset acc
  text_cts += dur;

  //send packet
  pck.send();

  //check if we should stop
  if (
    filter.dur.d &&
    text_cts >= Math.floor((1000 * filter.dur.n) / filter.dur.d)
  ) {
    print("done playing, cts " + text_cts);
    text_playing = false;
    text_pid.eos = true;
  }

  return GF_OK;
};
