/*
   mkvmerge -- utility for splicing together matroska files
   from component media subtypes

   Distributed under the GPL v2
   see the file COPYING for details
   or visit http://www.gnu.org/copyleft/gpl.html

   subtitle helper

   Written by Moritz Bunkus <moritz@bunkus.org>.
*/

#include "common/common_pch.h"

#include "common/endian.h"
#include "common/mime.h"
#include "common/mm_proxy_io.h"
#include "common/mm_text_io.h"
#include "common/strings/formatting.h"
#include "common/strings/parsing.h"
#include "common/strings/utf8.h"
#include "input/subtitles.h"
#include "merge/file_status.h"
#include "merge/input_x.h"
#include "merge/generic_reader.h"
#include "merge/packet_extensions.h"

// ------------------------------------------------------------

void
subtitles_c::process(generic_packetizer_c *p) {
  if (empty() || (entries.end() == current))
    return;

  packet_cptr packet(new packet_t(memory_c::borrow(current->subs), current->start, current->end - current->start));
  packet->extensions.push_back(packet_extension_cptr(new subtitle_number_packet_extension_c(current->number)));
  p->process(packet);
  ++current;
}

// ------------------------------------------------------------

#define SRT_RE_VALUE          "\\s*(-?)\\s*(\\d+)"
#define SRT_RE_TIMESTAMP      SRT_RE_VALUE ":" SRT_RE_VALUE ":" SRT_RE_VALUE "(?:[,\\.:]" SRT_RE_VALUE ")?"
#define SRT_RE_TIMESTAMP_LINE "^" SRT_RE_TIMESTAMP "\\s*[\\-\\s]+>\\s*" SRT_RE_TIMESTAMP "\\s*"
#define SRT_RE_COORDINATES    "([XY]\\d+:\\d+\\s*){4}\\s*$"

bool
srt_parser_c::probe(mm_text_io_c &io) {
  try {
    io.setFilePointer(0);
    std::string s;
    do {
      s = io.getline(10);
      strip(s);
    } while (s.empty());

    int64_t dummy;
    if (!parse_number(s, dummy))
      return false;

    s = io.getline(100);
    boost::regex timestamp_re(SRT_RE_TIMESTAMP_LINE, boost::regex::perl);
    if (!boost::regex_search(s, timestamp_re))
      return false;

    s = io.getline();
    io.setFilePointer(0);

  } catch (...) {
    return false;
  }

  return true;
}

srt_parser_c::srt_parser_c(mm_text_io_cptr const &io,
                           const std::string &file_name,
                           int64_t tid)
  : m_io(io)
  , m_file_name(file_name)
  , m_tid(tid)
  , m_coordinates_warning_shown(false)
{
}

void
srt_parser_c::parse() {
  boost::regex timestamp_re(SRT_RE_TIMESTAMP_LINE, boost::regex::perl);
  boost::regex number_re("^\\d+$", boost::regex::perl);
  boost::regex coordinates_re(SRT_RE_COORDINATES, boost::regex::perl);

  int64_t start                 = 0;
  int64_t end                   = 0;
  int64_t previous_start        = 0;
  bool timestamp_warning_printed = false;
  parser_state_e state          = STATE_INITIAL;
  int line_number               = 0;
  unsigned int subtitle_number  = 0;
  unsigned int timestamp_number  = 0;
  std::string subtitles;

  m_io->setFilePointer(0);

  while (1) {
    std::string s;
    if (!m_io->getline2(s))
      break;

    line_number++;
    strip_back(s);

    if (s.empty()) {
      if ((STATE_INITIAL == state) || (STATE_TIME == state))
        continue;

      state = STATE_SUBS_OR_NUMBER;

      if (!subtitles.empty())
        subtitles += "\n";
      subtitles += "\n";
      continue;
    }

    if (STATE_INITIAL == state) {
      if (!boost::regex_match(s, number_re)) {
        mxwarn_tid(m_file_name, m_tid, boost::format(Y("Error in line %1%: expected subtitle number and found some text.\n")) % line_number);
        break;
      }
      state = STATE_TIME;
      parse_number(s, subtitle_number);

    } else if (STATE_TIME == state) {
      boost::smatch matches;
      if (!boost::regex_search(s, matches, timestamp_re)) {
        mxwarn_tid(m_file_name, m_tid, boost::format(Y("Error in line %1%: expected a SRT timestamp line but found something else. Aborting this file.\n")) % line_number);
        break;
      }

      int64_t s_h = 0, s_min = 0, s_sec = 0, s_ns = 0, e_h = 0, e_min = 0, e_sec = 0, e_ns = 0;

      //        1         2       3      4        5     6                7     8
      // "\\s*(-?)\\s*(\\d+):\\s(-?)*(\\d+):\\s*(-?)(\\d+)(?:[,\\.]\\s*(-?)(\\d+))?"

      parse_number(matches[ 2].str(), s_h);
      parse_number(matches[ 4].str(), s_min);
      parse_number(matches[ 6].str(), s_sec);
      parse_number(matches[10].str(), e_h);
      parse_number(matches[12].str(), e_min);
      parse_number(matches[14].str(), e_sec);

      std::string s_rest = matches[ 8].str();
      std::string e_rest = matches[16].str();

      auto neg_calculator = [&matches](auto start_idx) -> auto {
        int64_t neg = 1;
        for (auto idx = start_idx; idx <= (start_idx + 6); idx += 2)
          neg *= matches[idx].str() == "-" ? -1 : 1;
        return neg;
      };

      int64_t s_neg = neg_calculator(1);
      int64_t e_neg = neg_calculator(9);

      if (boost::regex_search(s, coordinates_re) && !m_coordinates_warning_shown) {
        mxwarn_tid(m_file_name, m_tid,
                   Y("This file contains coordinates in the timestamp lines. "
                     "Such coordinates are not supported by the Matroska SRT subtitle format. "
                     "The coordinates will be removed automatically.\n"));
        m_coordinates_warning_shown = true;
      }

      // The previous entry is done now. Append it to the list of subtitles.
      if (!subtitles.empty()) {
        strip_back(subtitles, true);
        add(start, end, timestamp_number, subtitles.c_str());
      }

      while (s_rest.length() < 9)
        s_rest += "0";
      if (s_rest.length() > 9)
        s_rest.erase(9);

      while (e_rest.length() < 9)
        e_rest += "0";
      if (e_rest.length() > 9)
        e_rest.erase(9);

      parse_number(s_rest, s_ns);
      parse_number(e_rest, e_ns);

      // Calculate the start and end time in ns precision for the following entry.
      start  = ((s_h * 60 * 60 + s_min * 60 + s_sec) * 1'000'000'000ll + s_ns) * s_neg;
      end    = ((e_h * 60 * 60 + e_min * 60 + e_sec) * 1'000'000'000ll + e_ns) * e_neg;

      if (0 > start) {
        mxwarn_tid(m_file_name, m_tid,
                   boost::format(Y("Line %1%: Negative timestamp encountered. The entry will be adjusted to start from 00:00:00.000.\n")) % line_number);
        end   -= start;
        start  = 0;
        if (0 > end)
          end *= -1;
      }

      // There are files for which start timestamps overlap. Matroska requires
      // blocks to be sorted by their timestamp. mkvmerge does this at the end
      // of this function, but warn the user that the original order is being
      // changed.
      if (!timestamp_warning_printed && (start < previous_start)) {
        mxwarn_tid(m_file_name, m_tid, boost::format(Y("Warning in line %1%: The start timestamp is smaller than that of the previous entry. "
                                                       "All entries from this file will be sorted by their start time.\n")) % line_number);
        timestamp_warning_printed = true;
      }

      previous_start   = start;
      subtitles        = "";
      state            = STATE_SUBS;
      timestamp_number = subtitle_number;

    } else if (STATE_SUBS == state) {
      if (!subtitles.empty())
        subtitles += "\n";
      subtitles += s;

    } else if (boost::regex_match(s, number_re)) {
      state = STATE_TIME;
      parse_number(s, subtitle_number);

    } else {
      if (!subtitles.empty())
        subtitles += "\n";
      subtitles += s;
    }
  }

  if (!subtitles.empty()) {
    strip_back(subtitles, true);
    add(start, end, timestamp_number, subtitles.c_str());
  }

  sort();
}

// ------------------------------------------------------------

bool
ssa_parser_c::probe(mm_text_io_c &io) {
  boost::regex script_info_re("^\\s*\\[script\\s+info\\]",   boost::regex::perl | boost::regex::icase);
  boost::regex styles_re(     "^\\s*\\[V4\\+?\\s+Styles\\]", boost::regex::perl | boost::regex::icase);
  boost::regex comment_re(    "^\\s*$|^\\s*[!;]",            boost::regex::perl | boost::regex::icase);

  try {
    int line_number = 0;
    io.setFilePointer(0);

    std::string line;
    while (io.getline2(line, 1000)) {
      ++line_number;

      // Read at most 100 lines.
      if (100 < line_number)
        return false;

      // Skip comments and empty lines.
      if (boost::regex_search(line, comment_re))
        continue;

      // This is the line mkvmerge is looking for: positive match.
      if (boost::regex_search(line, script_info_re) || boost::regex_search(line, styles_re))
        return true;

      // Neither a wanted line nor an empty one/a comment: negative result.
      return false;
    }
  } catch (...) {
  }

  return false;
}

ssa_parser_c::ssa_parser_c(generic_reader_c &reader,
                           mm_text_io_cptr const &io,
                           const std::string &file_name,
                           int64_t tid)
  : m_reader(reader)
  , m_io(io)
  , m_file_name(file_name)
  , m_tid(tid)
  , m_cc_utf8(charset_converter_c::init("UTF-8"))
  , m_is_ass(false)
  , m_attachment_id(0)
{
}

void
ssa_parser_c::set_charset_converter(charset_converter_cptr const &cc_utf8) {
  if (cc_utf8)
    m_cc_utf8 = cc_utf8;

  else {
    m_cc_utf8  = g_cc_local_utf8;
    m_try_utf8 = true;
  }
}


void
ssa_parser_c::parse() {
  boost::regex sec_styles_ass_re("^\\s*\\[V4\\+\\s+Styles\\]", boost::regex::perl | boost::regex::icase);
  boost::regex sec_styles_re(    "^\\s*\\[V4\\s+Styles\\]",    boost::regex::perl | boost::regex::icase);
  boost::regex sec_info_re(      "^\\s*\\[Script\\s+Info\\]",  boost::regex::perl | boost::regex::icase);
  boost::regex sec_events_re(    "^\\s*\\[Events\\]",          boost::regex::perl | boost::regex::icase);
  boost::regex sec_graphics_re(  "^\\s*\\[Graphics\\]",        boost::regex::perl | boost::regex::icase);
  boost::regex sec_fonts_re(     "^\\s*\\[Fonts\\]",           boost::regex::perl | boost::regex::icase);

  int num                        = 0;
  ssa_section_e section          = SSA_SECTION_NONE;
  ssa_section_e previous_section = SSA_SECTION_NONE;
  std::string name_field         = "Name";

  std::string attachment_name, attachment_data_uu;

  m_io->setFilePointer(0);

  while (!m_io->eof()) {
    std::string line;
    if (!m_io->getline2(line))
      break;

    bool add_to_global = true;

    // A normal line. Let's see if this file is ASS and not SSA.
    if (!strcasecmp(line.c_str(), "ScriptType: v4.00+"))
      m_is_ass = true;

    else if (boost::regex_search(line, sec_styles_ass_re)) {
      m_is_ass = true;
      section  = SSA_SECTION_V4STYLES;

    } else if (boost::regex_search(line, sec_styles_re))
      section = SSA_SECTION_V4STYLES;

    else if (boost::regex_search(line, sec_info_re))
      section = SSA_SECTION_INFO;

    else if (boost::regex_search(line, sec_events_re))
      section = SSA_SECTION_EVENTS;

    else if (boost::regex_search(line, sec_graphics_re)) {
      section       = SSA_SECTION_GRAPHICS;
      add_to_global = false;

    } else if (boost::regex_search(line, sec_fonts_re)) {
      section       = SSA_SECTION_FONTS;
      add_to_global = false;

    } else if (SSA_SECTION_EVENTS == section) {
      if (balg::istarts_with(line, "Format: ")) {
        // Analyze the format string.
        m_format = split(&line.c_str()[strlen("Format: ")]);
        strip(m_format);

        // Let's see if "Actor" is used in the format instead of "Name".
        size_t i;
        for (i = 0; m_format.size() > i; ++i)
          if (balg::iequals(m_format[i], "actor")) {
            name_field = "Actor";
            break;
          }

      } else if (balg::istarts_with(line, "Dialogue: ")) {
        if (m_format.empty())
          throw mtx::input::extended_x(Y("ssa_reader: Invalid format. Could not find the \"Format\" line in the \"[Events]\" section."));

        std::string orig_line = line;

        line.erase(0, strlen("Dialogue: ")); // Trim the start.

        // Split the line into fields.
        std::vector<std::string> fields = split(line.c_str(), ",", m_format.size());
        while (fields.size() < m_format.size())
          fields.push_back(""s);

        // Parse the start time.
        auto stime = get_element("Start", fields);
        auto start = parse_time(stime);
        stime      = get_element("End", fields);
        auto end   = parse_time(stime);

        if (   (0     > start)
            || (0     > end)
            || (start > end)) {
          mxwarn_tid(m_file_name, m_tid, boost::format(Y("SSA/ASS: The following line will be skipped as one of the timestamps is less than 0, or the end timestamp is less than the start timestamp: %1%\n")) % orig_line);
          continue;
        }

        // Specs say that the following fields are to put into the block:
        // ReadOrder, Layer, Style, Name, MarginL, MarginR, MarginV, Effect,
        //   Text

        std::string comma = ",";
        line
          = to_string(num)                          + comma
          + get_element("Layer", fields)            + comma
          + get_element("Style", fields)            + comma
          + get_element(name_field.c_str(), fields) + comma
          + get_element("MarginL", fields)          + comma
          + get_element("MarginR", fields)          + comma
          + get_element("MarginV", fields)          + comma
          + get_element("Effect", fields)           + comma
          + recode(get_element("Text", fields));

        add(start, end, num, line);
        num++;

        add_to_global = false;
      }

    } else if ((SSA_SECTION_FONTS == section) || (SSA_SECTION_GRAPHICS == section)) {
      if (balg::istarts_with(line, "fontname:")) {
        add_attachment_maybe(attachment_name, attachment_data_uu, section);

        line.erase(0, strlen("fontname:"));
        strip(line, true);
        attachment_name = line;

      } else {
        strip(line, true);
        attachment_data_uu += line;
      }

      add_to_global = false;
    }

    if (add_to_global) {
      m_global += line;
      m_global += "\r\n";
    }

    if (previous_section != section)
      add_attachment_maybe(attachment_name, attachment_data_uu, previous_section);

    previous_section = section;
  }

  sort();
}

std::string
ssa_parser_c::get_element(const char *index,
                          std::vector<std::string> &fields) {
  size_t i;

  for (i = 0; i < m_format.size(); i++)
    if (m_format[i] == index)
      return fields[i];

  return ""s;
}

int64_t
ssa_parser_c::parse_time(std::string &stime) {
  int64_t th, tm, ts, tds;

  int pos = stime.find(':');
  if (0 > pos)
    return -1;

  std::string s = stime.substr(0, pos);
  if (!parse_number(s, th))
    return -1;
  stime.erase(0, pos + 1);

  pos = stime.find(':');
  if (0 > pos)
    return -1;

  s = stime.substr(0, pos);
  if (!parse_number(s, tm))
    return -1;
  stime.erase(0, pos + 1);

  pos = stime.find('.');
  if (0 > pos)
    return -1;

  s = stime.substr(0, pos);
  if (!parse_number(s, ts))
    return -1;
  stime.erase(0, pos + 1);

  if (!parse_number(stime, tds))
    return -1;

  return (tds * 10 + ts * 1000 + tm * 60 * 1000 + th * 60 * 60 * 1000) * 1000000;
}

std::string
ssa_parser_c::recode(std::string const &s,
                     uint32_t replacement_marker) {
  if (m_try_utf8 && !mtx::utf8::is_valid(s))
    m_try_utf8 = false;

  auto recoded = m_try_utf8 ? s : m_cc_utf8->utf8(s);

  if (mtx::utf8::is_valid(recoded))
    return recoded;

  if (!m_invalid_utf8_warned) {
    m_invalid_utf8_warned = true;
    mxwarn_tid(m_file_name, m_tid, boost::format(Y("This text subtitle track contains invalid 8-bit characters outside valid multi-byte UTF-8 sequences. Please specify the correct encoding for this track.\n")));
  }

  return mtx::utf8::fix_invalid(recoded, replacement_marker);
}

void
ssa_parser_c::add_attachment_maybe(std::string &name,
                                   std::string &data_uu,
                                   ssa_section_e section) {
  if (name.empty() || data_uu.empty() || ((SSA_SECTION_FONTS != section) && (SSA_SECTION_GRAPHICS != section))) {
    name    = "";
    data_uu = "";
    return;
  }

  ++m_attachment_id;

  if (!m_reader.attachment_requested(m_attachment_id)) {
    name    = "";
    data_uu = "";
    return;
  }

  auto attachment_p = std::make_shared<attachment_t>();
  auto &attachment  = *attachment_p;

  std::string short_name = m_file_name;
  size_t pos             = short_name.rfind('/');

  if (std::string::npos != pos)
    short_name.erase(0, pos + 1);
  pos = short_name.rfind('\\');
  if (std::string::npos != pos)
    short_name.erase(0, pos + 1);

  attachment.ui_id        = m_attachment_id;
  attachment.name         = recode(name, static_cast<uint32_t>('_'));
  attachment.description  = (boost::format(SSA_SECTION_FONTS == section ? Y("Imported font from %1%") : Y("Imported picture from %1%")) % short_name).str();
  attachment.to_all_files = true;
  attachment.source_file  = m_file_name;

  size_t data_size        = data_uu.length() % 4;
  data_size               = 3 == data_size ? 2 : 2 == data_size ? 1 : 0;
  data_size              += data_uu.length() / 4 * 3;
  attachment.data         = memory_c::alloc(data_size);
  auto out                = attachment.data->get_buffer();
  auto in                 = reinterpret_cast<unsigned char const *>(data_uu.c_str());

  for (auto end = in + (data_uu.length() / 4) * 4; in < end; in += 4, out += 3)
    decode_chars(in, out, 4);

  decode_chars(in, out, data_uu.length() % 4);

  attachment.mime_type = mtx::mime::guess_type(name, false);

  if (attachment.mime_type == "")
    attachment.mime_type = "application/octet-stream";

  add_attachment(attachment_p);

  name    = "";
  data_uu = "";
}

void
ssa_parser_c::decode_chars(unsigned char const *in,
                           unsigned char *out,
                           size_t bytes_in) {
  if (!bytes_in)
    return;

  size_t bytes_out = 4 == bytes_in ? 3 : 3 == bytes_in ? 2 : 1;
  uint32_t value   = 0;

  for (int idx = 0; idx < static_cast<int>(bytes_in); ++idx)
    value |= (static_cast<uint32_t>(in[idx]) - 33) << (6 * (3 - idx));

  for (int idx = 0; idx < static_cast<int>(bytes_out); ++idx)
    out[idx] = (value >> ((2 - idx) * 8)) & 0xff;
}
