/*
 * Copyright © 2025  Adobe, Inc.
 *
 *  This is part of HarfBuzz, a text shaping library.
 *
 * Permission is hereby granted, without written agreement and without
 * license or royalty fees, to use, copy, modify, and distribute this
 * software and its documentation for any purpose, provided that the
 * above copyright notice and the following two paragraphs appear in
 * all copies of this software.
 *
 * IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE TO ANY PARTY FOR
 * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES
 * ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN
 * IF THE COPYRIGHT HOLDER HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
 * DAMAGE.
 *
 * THE COPYRIGHT HOLDER SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING,
 * BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
 * FITNESS FOR A PARTICULAR PURPOSE.  THE SOFTWARE PROVIDED HEREUNDER IS
 * ON AN "AS IS" BASIS, AND THE COPYRIGHT HOLDER HAS NO OBLIGATION TO
 * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 *
 * Adobe Author(s): Skef Iterum
 */

#include "batch.hh"
#include "face-options.hh"

#include <hb-subset.h>

#include <set>
#include <string>
#include <vector>

struct depend_t : option_parser_t, face_options_t
{
  gboolean flagged_only = false;
  gboolean no_context = false;
  gboolean no_glyph_names = false;
  gboolean compile_only = false;
  gboolean quiet = false;
  char *gids_str = nullptr;

  void
  add_options ()
  {
    set_summary ("Show glyph dependency graph.");
    set_description ("Prints the glyph dependency graph extracted from a font file.\n"
                     "For each source glyph, lists the glyphs it can produce or depend on\n"
                     "through GSUB substitutions, composite glyphs, COLR layers, and MATH variants.");

    face_options_t::add_options (this);

    GOptionEntry depend_entries[] =
    {
      {"gids",           0, 0, G_OPTION_ARG_STRING, &this->gids_str,
       "Filter output to specific glyph IDs (e.g. '1,5,10-20')", "GIDs"},
      {"flagged",        0, 0, G_OPTION_ARG_NONE, &this->flagged_only,
       "Show only edges with over-approximation flags set (C or N)", nullptr},
      {"no-context",     0, 0, G_OPTION_ARG_NONE, &this->no_context,
       "Suppress context set output", nullptr},
      {"no-glyph-names", 0, 0, G_OPTION_ARG_NONE, &this->no_glyph_names,
       "Use numeric glyph IDs instead of names", nullptr},
      {"compile",        0, 0, G_OPTION_ARG_NONE, &this->compile_only,
       "Build the dependency graph without retrieving it", nullptr},
      {"quiet",          'q', 0, G_OPTION_ARG_NONE, &this->quiet,
       "Retrieve the dependency graph without printing it", nullptr},
      {nullptr}
    };
    add_group (depend_entries,
               "depend",
               "Dependency graph options:",
               "Options for filtering and formatting the dependency graph output",
               this,
               false);

    GOptionEntry entries[] =
    {
      {G_OPTION_REMAINING, 0, G_OPTION_FLAG_IN_MAIN,
       G_OPTION_ARG_CALLBACK, (gpointer) &collect_rest, nullptr, "[FONT-FILE]"},
      {nullptr}
    };
    add_main_group (entries, this);

    option_parser_t::add_options ();
  }

  static gboolean
  collect_rest (const char *name G_GNUC_UNUSED,
                const char *arg,
                gpointer    data,
                GError    **error)
  {
    depend_t *thiz = (depend_t *) data;

    if (!thiz->font_file)
    {
      thiz->font_file = g_strdup (arg);
      return true;
    }

    g_set_error (error, G_OPTION_ERROR, G_OPTION_ERROR_FAILED,
                 "Too many arguments on the command line");
    return false;
  }

  /* Returns the glyph name if font is non-null and the font has a real name
   * for this glyph; otherwise returns the glyph ID as a decimal string. */
  static std::string
  glyph_str (hb_font_t *font, hb_codepoint_t gid)
  {
    if (font)
    {
      char buf[128];
      if (hb_font_get_glyph_name (font, gid, buf, sizeof (buf)))
        return buf;
    }
    char buf[32];
    snprintf (buf, sizeof (buf), "%u", gid);
    return buf;
  }

  /* Parses a comma-separated list of GID ranges (e.g. "1,5,10-20,42"). */
  static std::set<unsigned>
  parse_gids (const char *str)
  {
    std::set<unsigned> result;
    const char *p = str;
    while (*p)
    {
      char *end;
      unsigned long start = strtoul (p, &end, 10);
      if (end == p) break;
      p = end;
      if (*p == '-')
      {
        p++;
        unsigned long stop = strtoul (p, &end, 10);
        if (end == p) break;
        p = end;
        for (unsigned long i = start; i <= stop; i++)
          result.insert ((unsigned) i);
      }
      else
        result.insert ((unsigned) start);
      if (*p == ',') p++;
    }
    return result;
  }

  /* Builds a human-readable string representing the context set at ctx_idx.
   * Elements < 0x80000000 are direct GID refs joined with &.
   * Elements >= 0x80000000 are indirect set refs (strip high bit for index),
   * printed as parenthesized |-separated members. */
  static std::string
  build_context_str (hb_subset_depend_t *depend, hb_codepoint_t ctx_idx,
                     hb_font_t *font)
  {
    hb_set_t *ctx_set = hb_set_create ();
    if (!hb_subset_depend_lookup_set (depend, ctx_idx, ctx_set))
    {
      hb_set_destroy (ctx_set);
      return "";
    }

    std::string result = "(";
    bool first = true;
    hb_codepoint_t elem = HB_SET_VALUE_INVALID;
    while (hb_set_next (ctx_set, &elem))
    {
      if (!first) result += " & ";
      first = false;
      if (elem < 0x80000000u)
      {
        result += glyph_str (font, elem);
      }
      else
      {
        hb_codepoint_t set_idx = elem & 0x7FFFFFFFu;
        hb_set_t *sub_set = hb_set_create ();
        if (hb_subset_depend_lookup_set (depend, set_idx, sub_set))
        {
          result += "(";
          bool sub_first = true;
          hb_codepoint_t sub_elem = HB_SET_VALUE_INVALID;
          while (hb_set_next (sub_set, &sub_elem))
          {
            if (!sub_first) result += "|";
            sub_first = false;
            result += glyph_str (font, sub_elem);
          }
          result += ")";
        }
        hb_set_destroy (sub_set);
      }
    }
    result += ")";

    hb_set_destroy (ctx_set);
    return result;
  }

  int
  operator () (int argc, char **argv)
  {
    add_options ();

    parse (&argc, &argv);

    hb_subset_depend_t *depend = hb_subset_depend_from_face_or_fail (face);
    if (!depend)
    {
      fprintf (stderr, "Failed to build dependency graph\n");
      return 1;
    }

    if (compile_only)
    {
      hb_subset_depend_destroy (depend);
      return 0;
    }

    hb_font_t *font = no_glyph_names || quiet ? nullptr : hb_font_create (face);

    std::set<unsigned> gid_filter;
    bool filter_gids = gids_str != nullptr;
    if (filter_gids)
      gid_filter = parse_gids (gids_str);

    unsigned num_glyphs = hb_face_get_glyph_count (face);
    std::set<hb_codepoint_t> seen_lig_sets;
    bool printed_any = false;

    for (unsigned gid = 0; gid < num_glyphs; gid++)
    {
      if (filter_gids && !gid_filter.count (gid))
        continue;

      unsigned total = hb_subset_depend_lookup_glyph (depend, gid, 0, nullptr, nullptr);
      if (total == 0) continue;

      /* Collect entries, applying the --flagged filter */
      std::vector<hb_subset_depend_entry_t> entries;
      for (unsigned i = 0; i < total; i++)
      {
        hb_subset_depend_entry_t entry;
        unsigned count = 1;
        hb_subset_depend_lookup_glyph (depend, gid, i, &count, &entry);
        if (flagged_only && entry.flags == HB_SUBSET_DEPEND_EDGE_FLAG_NONE)
          continue;
        entries.push_back (entry);
      }

      if (entries.empty () || quiet) continue;

      if (printed_any) printf ("\n");
      printed_any = true;

      /* Source GID header: "42 (glyphname):" or "42:" */
      {
        char name_buf[128];
        if (font && hb_font_get_glyph_name (font, gid, name_buf, sizeof (name_buf)))
          printf ("%u (%s):\n", gid, name_buf);
        else
          printf ("%u:\n", gid);
      }

      for (const auto &entry : entries)
      {
        std::string line = "  ";

        /* Table tag, trailing spaces trimmed (e.g. "CFF " -> "CFF") */
        char tag_buf[5];
        hb_tag_to_string (entry.table_tag, tag_buf);
        tag_buf[4] = 0;
        int tag_end = 4;
        while (tag_end > 0 && tag_buf[tag_end - 1] == ' ') tag_end--;
        tag_buf[tag_end] = 0;
        line += tag_buf;

        /* Feature tag only for GSUB edges */
        if (entry.table_tag == HB_OT_TAG_GSUB)
        {
          char feat_buf[5];
          hb_tag_to_string (entry.layout_tag, feat_buf);
          feat_buf[4] = 0;
          line += " ";
          line += feat_buf;
        }

        line += " -> ";
        line += glyph_str (font, entry.dependent);

        if (entry.ligature_set_index != HB_CODEPOINT_INVALID)
        {
          char buf[32];
          snprintf (buf, sizeof (buf), "  lig[%u]", entry.ligature_set_index);
          line += buf;
        }

        if (entry.flags)
        {
          line += "  flags: ";
          if (entry.flags & HB_SUBSET_DEPEND_EDGE_FLAG_FROM_CONTEXT_POSITION) line += "C";
          if (entry.flags & HB_SUBSET_DEPEND_EDGE_FLAG_FROM_NESTED_CONTEXT)   line += "N";
        }

        /* Context: append inline if it fits within 80 columns, else wrap */
        bool ctx_wraps = false;
        std::string ctx_str;
        if (!no_context && entry.context_set_index != HB_CODEPOINT_INVALID)
        {
          ctx_str = build_context_str (depend, entry.context_set_index, font);
          if (!ctx_str.empty ())
          {
            if (line.size () + 2 + ctx_str.size () <= 80)
              line += "  " + ctx_str;
            else
              ctx_wraps = true;
          }
        }

        printf ("%s\n", line.c_str ());

        if (ctx_wraps)
          printf ("    ctx: %s\n", ctx_str.c_str ());

        /* Ligature set contents: printed once (on first occurrence) */
        if (entry.ligature_set_index != HB_CODEPOINT_INVALID &&
            !seen_lig_sets.count (entry.ligature_set_index))
        {
          seen_lig_sets.insert (entry.ligature_set_index);
          hb_set_t *lig_set = hb_set_create ();
          if (hb_subset_depend_lookup_set (depend, entry.ligature_set_index, lig_set))
          {
            printf ("    lig[%u]:", entry.ligature_set_index);
            hb_codepoint_t lig_gid = HB_SET_VALUE_INVALID;
            while (hb_set_next (lig_set, &lig_gid))
              printf (" %s", glyph_str (font, lig_gid).c_str ());
            printf ("\n");
          }
          hb_set_destroy (lig_set);
        }
      }
    }

    if (font) hb_font_destroy (font);
    hb_subset_depend_destroy (depend);

    return 0;
  }
};

int
main (int argc, char **argv)
{
  return batch_main<depend_t> (argc, argv);
}
