/*
 * Copyright © 2025  Google, 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.
 *
 * Google Author(s): Garret Rieger
 */

#ifndef GRAPH_LIGATURE_GRAPH_HH
#define GRAPH_LIGATURE_GRAPH_HH

#include "graph.hh"
#include "../OT/Layout/GSUB/LigatureSubst.hh"
#include "../OT/Layout/GSUB/LigatureSubstFormat1.hh"
#include "../OT/Layout/GSUB/LigatureSet.hh"
#include "../OT/Layout/types.hh"
#include <algorithm>
#include <utility>

namespace graph {

struct LigatureSet : public OT::Layout::GSUB_impl::LigatureSet<SmallTypes>
{
  bool sanitize (graph_t::vertex_t& vertex) const
  {
    size_t vertex_len = vertex.obj.tail - vertex.obj.head;
    if (vertex_len < OT::Layout::GSUB_impl::LigatureSet<SmallTypes>::min_size) return false;
    hb_barrier ();

    size_t total_len = ligature.get_size() + OT::Layout::GSUB_impl::LigatureSet<SmallTypes>::min_size - ligature.len.get_size();
    if (vertex_len < total_len) {
      return false;
    }
    return true;
  }
};

struct LigatureSubstFormat1 : public OT::Layout::GSUB_impl::LigatureSubstFormat1_2<SmallTypes>
{
  bool sanitize (graph_t::vertex_t& vertex) const
  {
    size_t vertex_len = vertex.obj.tail - vertex.obj.head;
    unsigned min_size = OT::Layout::GSUB_impl::LigatureSubstFormat1_2<SmallTypes>::min_size;
    if (vertex_len < min_size) return false;
    hb_barrier ();

    return vertex_len >=
        min_size + ligatureSet.get_size() - ligatureSet.len.get_size();
  }

  hb_vector_t<unsigned> split_subtables (gsubgpos_graph_context_t& c,
                                         unsigned this_index)
  {
    auto split_points = compute_split_points(c, this_index);
    split_context_t split_context {
      c,
      this,
      this_index,
      total_number_ligas(c, this_index),
      liga_counts(c, this_index),
    };
    return actuate_subtable_split<split_context_t> (split_context, split_points);
  }

 private:
  unsigned total_number_ligas(gsubgpos_graph_context_t& c, unsigned this_index) const {
    unsigned total = 0;
    for (unsigned i = 0; i < ligatureSet.len; i++)
    {
      auto liga_set = c.graph.as_table<LigatureSet>(this_index, &ligatureSet[i]);
      if (!liga_set.table) {
        return 0;
      }
      total += liga_set.table->ligature.len;
    }
    return total;
  }

  hb_vector_t<unsigned> liga_counts(gsubgpos_graph_context_t& c, unsigned this_index) const {
    hb_vector_t<unsigned> result;
    for (unsigned i = 0; i < ligatureSet.len; i++)
    {
      auto liga_set = c.graph.as_table<LigatureSet>(this_index, &ligatureSet[i]);
      result.push(!liga_set.table ? 0 : liga_set.table->ligature.len);
    }
    return result;
  }

  hb_vector_t<unsigned> ligature_index_to_object_id(const graph_t::vertex_and_table_t<LigatureSet>& liga_set) const {
    hb_vector_t<unsigned> map;
    map.resize_exact(liga_set.table->ligature.len);
    if (map.in_error()) return map;

    for (unsigned i = 0; i < map.length; i++) {
      map[i] = (unsigned) -1;
    }

    for (const auto& l : liga_set.vertex->obj.real_links) {
      if (l.position < 2) continue;
      unsigned array_index = (l.position - 2) / 2;
      map[array_index] = l.objidx;
    }
    return map;
  }

  hb_vector_t<unsigned> compute_split_points(gsubgpos_graph_context_t& c,
                                             unsigned this_index) const
  {
    // For ligature subst coverage is always packed last, and as a result is where an overflow
    // will happen if there is one, so we can check the estimate length of the
    // LigatureSubstFormat1 -> Coverage offset length which is the sum of all data in the
    // retained sub graph except for the coverage table itself.
    const unsigned base_size = OT::Layout::GSUB_impl::LigatureSubstFormat1_2<SmallTypes>::min_size;
    unsigned accumulated = base_size;

    unsigned ligature_index = 0;
    hb_vector_t<unsigned> split_points;
    for (unsigned i = 0; i < ligatureSet.len; i++)
    {
      accumulated += OT::HBUINT16::static_size; // for ligature set offset
      accumulated += OT::Layout::GSUB_impl::LigatureSet<SmallTypes>::min_size; // for ligature set table

      auto liga_set = c.graph.as_table<LigatureSet>(this_index, &ligatureSet[i]);
      if (!liga_set.table) {
        return hb_vector_t<unsigned> {};
      }

      // Finding the object id associated with an array index is O(n)
      // so to avoid O(n^2), precompute the mapping by scanning through
      // all links
      auto index_to_id = ligature_index_to_object_id(liga_set);
      if (index_to_id.in_error()) return hb_vector_t<unsigned>();

      for (unsigned j = 0; j < liga_set.table->ligature.len; j++)
      {
        const unsigned liga_id = index_to_id[j];
        if (liga_id == (unsigned) -1) continue; // no outgoing link, ignore
        const unsigned liga_size = c.graph.vertices_[liga_id].table_size ();

        accumulated += OT::HBUINT16::static_size; // for ligature offset
        accumulated += liga_size; // for the ligature table

        if (accumulated >= (1 << 16))
        {
          split_points.push(ligature_index);
          // We're going to split such that the current ligature will be in the new sub table.
          // That means we'll have one ligature subst (base_base), one ligature set, and one liga table
          accumulated = base_size + // for liga subst subtable
            (OT::HBUINT16::static_size * 2) + // for liga set and liga offset
            OT::Layout::GSUB_impl::LigatureSet<SmallTypes>::min_size + // for liga set subtable
            liga_size; // for liga sub table
        }

        ligature_index++;
      }
    }

    return split_points;
  }

  struct split_context_t
  {
    gsubgpos_graph_context_t& c;
    LigatureSubstFormat1* thiz;
    unsigned this_index;
    unsigned original_count_;
    hb_vector_t<unsigned> liga_counts;

    unsigned original_count ()
    {
      return original_count_;
    }

    unsigned clone_range (unsigned start, unsigned end)
    {
      return thiz->clone_range (c, this_index, liga_counts, start, end);
    }

    bool shrink (unsigned count)
    {
      return thiz->shrink (c, this_index, original_count(), liga_counts, count);
    }
  };

  hb_pair_t<unsigned, LigatureSet*> new_liga_set(gsubgpos_graph_context_t& c, unsigned count) const {
    unsigned prime_size = OT::Layout::GSUB_impl::LigatureSet<SmallTypes>::min_size
                          + count * SmallTypes::size;

    unsigned prime_id = c.create_node (prime_size);
    if (prime_id == (unsigned) -1) return hb_pair(-1, nullptr);

    LigatureSet* prime = (LigatureSet*) c.graph.object (prime_id).head;
    prime->ligature.len = count;
    return hb_pair(prime_id, prime);
  }

  void clear_virtual_links (gsubgpos_graph_context_t& c, unsigned node_index) const
  {
    auto& obj = c.graph.vertices_[node_index].obj;
    for (const auto& l : obj.virtual_links)
    {
      auto& child = c.graph.vertices_[l.objidx];
      child.remove_parent(node_index);
    }
    obj.virtual_links.clear();
  }

  void add_virtual_link(gsubgpos_graph_context_t& c, unsigned from, unsigned to) const {
    auto& from_obj = c.graph.vertices_[from].obj;
    c.graph.vertices_[to].add_parent(from, true);
    auto& link = *from_obj.virtual_links.push ();
    link.objidx = to;
  }

  hb_pair_t<unsigned, unsigned> current_liga_set_bounds (gsubgpos_graph_context_t& c,
                                                         unsigned liga_set_index,
                                                         const hb_serialize_context_t::object_t& liga_set) const
  {
    // Finds the actual liga indices present in the liga set currently. Takes
    // into account those that have been removed by processing.
    unsigned min_index = (unsigned) -1;
    unsigned max_index = 0;
    for (const auto& l : liga_set.real_links) {
      if (l.position < 2) continue;

      unsigned liga_index = (l.position - 2) / 2;
      min_index = hb_min(min_index, liga_index);
      max_index = hb_max(max_index, liga_index);
    }
    return hb_pair(min_index, max_index + 1);
  }

  void compact_liga_set (gsubgpos_graph_context_t& c, LigatureSet* table, hb_serialize_context_t::object_t& obj) const
  {
    if (table->ligature.len <= obj.real_links.length) return;

    // compact the remaining linked liga offsets into a continous array and shrink the node as needed.
    unsigned to_remove = table->ligature.len - obj.real_links.length;
    unsigned new_position = SmallTypes::size;
    obj.real_links.qsort(); // for this to work we need to process links in order of position.
    for (auto& l : obj.real_links)
    {
      l.position = new_position;
      new_position += SmallTypes::size;
    }

    table->ligature.len = obj.real_links.length;
    obj.tail -= to_remove * SmallTypes::size;
  }

  unsigned clone_range (gsubgpos_graph_context_t& c,
                        unsigned this_index,
                        hb_vector_t<unsigned> liga_counts,
                        unsigned start, unsigned end) const
  {
    DEBUG_MSG (SUBSET_REPACK, nullptr,
               "  Cloning LigatureSubstFormat1 (%u) range [%u, %u).", this_index, start, end);

    // Create an oversized new liga subst, we'll adjust the size down later. We don't know
    // the final size until we process it but we also need it to exist while we're processing
    // so that nodes can be moved to it as needed.
    unsigned prime_size = OT::Layout::GSUB_impl::LigatureSubstFormat1_2<SmallTypes>::min_size
                          + ligatureSet.get_size() - ligatureSet.len.get_size();

    unsigned liga_subst_prime_id = c.create_node (prime_size);
    if (liga_subst_prime_id == (unsigned) -1) return -1;

    LigatureSubstFormat1* liga_subst_prime = (LigatureSubstFormat1*) c.graph.object (liga_subst_prime_id).head;
    liga_subst_prime->format = this->format;
    liga_subst_prime->ligatureSet.len = this->ligatureSet.len;

    // Create a place holder coverage prime id since we need to add virtual links to it while
    // generating liga and liga sets. Afterwards it will be updated to have the correct coverage.
    unsigned coverage_id = c.graph.index_for_offset (this_index, &coverage);
    unsigned coverage_prime_id = c.graph.duplicate(coverage_id);
    auto& coverage_prime_vertex = c.graph.vertices_[coverage_prime_id];
    auto* coverage_prime_link = c.graph.vertices_[liga_subst_prime_id].obj.real_links.push ();
    coverage_prime_link->width = SmallTypes::size;
    coverage_prime_link->objidx = coverage_prime_id;
    coverage_prime_link->position = 2;
    coverage_prime_vertex.add_parent (liga_subst_prime_id, false);

    // Locate all liga sets with ligas between start and end.
    // Clone or move them as needed.
    unsigned count = 0;
    unsigned liga_set_count = 0;
    unsigned liga_set_start = -1;
    unsigned liga_set_end = 0; // inclusive
    for (unsigned i = 0; i < liga_counts.length; i++)
    {
      unsigned num_ligas = liga_counts[i];

      unsigned current_start = count;
      unsigned current_end = count + num_ligas;

      if (current_start >= end || start >= current_end) {
        // No intersection, so just skip
        count += num_ligas;
        continue;
      }

      auto liga_set_index = c.graph.index_for_offset(this_index, &ligatureSet[i]);
      auto liga_set = c.graph.as_table<LigatureSet>(this_index, &ligatureSet[i]);
      if (!liga_set.table) {
        return -1;
      }

      // Bounds may need to be adjusted if some ligas have been previously removed.
      hb_pair_t<unsigned, unsigned> liga_bounds = current_liga_set_bounds(c, liga_set_index, liga_set.vertex->obj);
      current_start = hb_max(count + liga_bounds.first, current_start);
      current_end = hb_min(count + liga_bounds.second, current_end);

      unsigned liga_set_prime_id;
      if (current_start >= start && current_end <= end) {
        // This liga set is fully contined within [start, end)
        // We can move the entire ligaset to the new liga subset object.
        liga_set_end = i;
        if (i < liga_set_start) liga_set_start = i;
        liga_set_prime_id = c.graph.move_child<> (this_index,
                              &ligatureSet[i],
                              liga_subst_prime_id,
                              &liga_subst_prime->ligatureSet[liga_set_count++]);
        compact_liga_set(c, liga_set.table, liga_set.vertex->obj);
      }
      else
      {
        // This liga set partially overlaps [start, end). We'll need to create
        // a new liga set sub table and move the intersecting ligas to it.
        unsigned start_index = hb_max(start, current_start) - count;
        unsigned end_index = hb_min(end, current_end) - count;
        unsigned liga_count = end_index - start_index;
        auto result = new_liga_set(c, liga_count);
        liga_set_prime_id = result.first;
        if (liga_set_prime_id == (unsigned) -1) return -1;

        c.graph.move_children<OT::Offset16>(
          liga_set_index,
          2 + start_index * 2,
          2 + end_index * 2,
          liga_set_prime_id,
          2);

        liga_set_end = i;
        if (i < liga_set_start) liga_set_start = i;
        c.graph.add_link(&liga_subst_prime->ligatureSet[liga_set_count++], liga_subst_prime_id, liga_set_prime_id);
      }

      // The new liga and all children set needs to have a virtual link to the new coverage table:
      auto& liga_set_prime = c.graph.vertices_[liga_set_prime_id].obj;
      clear_virtual_links(c, liga_set_prime_id);
      add_virtual_link(c, liga_set_prime_id, coverage_prime_id);
      for (const auto& l : liga_set_prime.real_links) {
        clear_virtual_links(c, l.objidx);
        add_virtual_link(c, l.objidx, coverage_prime_id);
      }

      count += num_ligas;
    }

    c.graph.vertices_[liga_subst_prime_id].obj.tail -= (liga_subst_prime->ligatureSet.len - liga_set_count) * SmallTypes::size;
    liga_subst_prime->ligatureSet.len = liga_set_count;

    if (!Coverage::filter_coverage (c,
                                    coverage_prime_id,
                                    liga_set_start, liga_set_end + 1))
      return -1;

    return liga_subst_prime_id;
  }

  bool shrink (gsubgpos_graph_context_t& c,
               unsigned this_index,
               unsigned old_count,
               hb_vector_t<unsigned> liga_counts,
               unsigned count)
  {
    DEBUG_MSG (SUBSET_REPACK, nullptr,
               "  Shrinking LigatureSubstFormat1 (%u) to [0, %u).",
               this_index,
               count);
    if (count >= old_count)
      return true;

    hb_set_t retained_indices;
    unsigned new_liga_set_count = 0;
    for (unsigned i = 0; i < liga_counts.length; i++)
    {
      auto liga_set = c.graph.as_table<LigatureSet>(this_index, &ligatureSet[i]);
      if (!liga_set.table) {
        return false;
      }

      // We need the virtual links to coverage removed from all descendants on this liga subst.
      // If any are left when we try to mutate the coverage table later it will be unnessecarily
      // duplicated. Code later on will re-add the virtual links as needed (via retained_indices).
      clear_virtual_links(c, liga_set.index);
      retained_indices.add(liga_set.index);

      auto index_to_id = ligature_index_to_object_id(liga_set);
      if (index_to_id.in_error()) return false;

      for (unsigned i = 0; i < liga_set.table->ligature.len; i++) {
        unsigned liga_index = index_to_id[i];
        if (liga_index != (unsigned) -1) {
          clear_virtual_links(c, liga_index);
          retained_indices.add(liga_index);
        }
      }

      unsigned num_ligas = liga_counts[i];
      if (num_ligas >= count) {
        // drop the trailing liga's from this set and all subsequent liga sets
        unsigned num_ligas_to_remove = num_ligas - count;
        new_liga_set_count = i + 1;
        c.graph.vertices_[liga_set.index].obj.tail -= num_ligas_to_remove * SmallTypes::size;
        liga_set.table->ligature.len = count;
        break;
      } else {
        count -= num_ligas;
      }
    }

    // Adjust liga set array
    auto& this_vertex = c.graph.vertices_[this_index];
    this_vertex.obj.tail -= (ligatureSet.len - new_liga_set_count) * SmallTypes::size;
    ligatureSet.len = new_liga_set_count;

    // Coverage matches the number of liga sets so rebuild as needed
    unsigned coverage_idx = c.graph.index_for_offset (this_index, &this->coverage);
    if (coverage_idx == (unsigned) -1) return false;

    auto& coverage_v = c.graph.vertices_[coverage_idx];
    unsigned coverage_size = coverage_v.table_size ();
    Coverage* coverage_table = (Coverage*) coverage_v.obj.head;

    if (coverage_v.is_shared ())
    {
      coverage_idx = c.graph.remap_child (this_index, coverage_idx);
      if (coverage_idx == (unsigned) -1) return false;
    }

    for (unsigned i : retained_indices.iter())
      add_virtual_link(c, i, coverage_idx);

    auto new_coverage =
        + hb_zip (coverage_table->iter (), hb_range ())
        | hb_filter ([&] (hb_pair_t<unsigned, unsigned> p) {
          return p.second < new_liga_set_count;
        })
        | hb_map_retains_sorting (hb_first)
        ;

    return Coverage::make_coverage (c, new_coverage, coverage_idx, coverage_size);
  }
};

struct LigatureSubst : public OT::Layout::GSUB_impl::LigatureSubst
{

  hb_vector_t<unsigned> split_subtables (gsubgpos_graph_context_t& c,
                                         unsigned this_index)
  {
    switch (u.format.v) {
    case 1:
      return ((LigatureSubstFormat1*)(&u.format1))->split_subtables (c, this_index);
#ifndef HB_NO_BEYOND_64K
    case 2: HB_FALLTHROUGH;
      // Don't split 24bit Ligature Subs
#endif
    default:
      return hb_vector_t<unsigned> ();
    }
  }

  bool sanitize (graph_t::vertex_t& vertex) const
  {
    size_t vertex_len = vertex.obj.tail - vertex.obj.head;
    if (vertex_len < u.format.v.get_size ()) return false;
    hb_barrier ();

    switch (u.format.v) {
    case 1:
      return ((LigatureSubstFormat1*)(&u.format1))->sanitize (vertex);
#ifndef HB_NO_BEYOND_64K
    case 2:  HB_FALLTHROUGH;
#endif
    default:
      // We don't handle format 2 here.
      return false;
    }
  }
};

}

#endif  // GRAPH_LIGATURE_GRAPH_HH
