// OpenSTA, Static Timing Analyzer
// Copyright (c) 2026, Parallax Software, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
//
// The origin of this software must not be misrepresented; you must not
// claim that you wrote the original software.
//
// Altered source versions must be plainly marked as such, and must not be
// misrepresented as being the original software.
//
// This notice may not be removed or altered from any source distribution.

#include "TagGroup.hh"

#include "ClkInfo.hh"
#include "Debug.hh"
#include "Graph.hh"
#include "Path.hh"
#include "Report.hh"
#include "Scene.hh"
#include "Search.hh"
#include "Tag.hh"

namespace sta {

TagGroup::TagGroup(TagGroupIndex index,
                   PathIndexMap *path_index_map,
                   bool has_clk_tag,
                   bool has_genclk_src_tag,
                   bool has_filter_tag,
                   bool has_loop_tag,
                   const StaState *sta) :
  path_index_map_(path_index_map),
  hash_(hash(path_index_map,
             sta)),
  ref_count_(0),
  index_(index),
  has_clk_tag_(has_clk_tag),
  has_genclk_src_tag_(has_genclk_src_tag),
  has_filter_tag_(has_filter_tag),
  has_loop_tag_(has_loop_tag),
  own_path_map_(true)
{
}

TagGroup::TagGroup(TagGroupBldr *tag_bldr,
                   const StaState *sta) :
  path_index_map_(&tag_bldr->pathIndexMap()),
  hash_(hash(path_index_map_,
             sta)),
  ref_count_(0),
  own_path_map_(false)
{
}

TagGroup::~TagGroup()
{
  if (own_path_map_)
    delete path_index_map_;
}

void
TagGroup::incrRefCount()
{
  ref_count_++;
}

void
TagGroup::decrRefCount()
{
  ref_count_--;
}

size_t
TagGroup::hash(PathIndexMap *path_index_map,
               const StaState *sta)
{
  size_t hash = 0;
  for (auto const [tag, path_index] : *path_index_map)
    hash += tag->hash(true, sta);
  return hash;
}

bool
TagGroup::hasTag(Tag *tag) const
{
  return path_index_map_->contains(tag);
}

size_t
TagGroup::pathIndex(Tag *tag) const
{
  size_t path_index;
  bool exists;
  findKeyValue(path_index_map_, tag, path_index, exists);
  if (exists)
    return path_index;
  else
    return 0;
}

void
TagGroup::pathIndex(Tag *tag,
                    size_t &path_index,
                    bool &exists) const
{
  findKeyValue(path_index_map_, tag, path_index, exists);
}

void
TagGroup::report(const StaState *sta) const
{
  Report *report = sta->report();
  report->report("Group {} hash = {}", index_, hash_);
  pathIndexMapReport(path_index_map_, sta);
}

void
TagGroup::reportArrivalMap(const StaState *sta) const
{
  pathIndexMapReport(path_index_map_, sta);
}

void
pathIndexMapReport(const PathIndexMap *path_index_map,
                   const StaState *sta)
{
  Report *report = sta->report();
  for (auto const [tag, path_index] : *path_index_map)
    report->report(" {:2} {}", path_index, tag->to_string(sta));
  report->reportBlankLine();
}

////////////////////////////////////////////////////////////////

TagGroupBldr::TagGroupBldr(bool match_crpr_clk_pin,
                           const StaState *sta) :
  default_path_count_(sta->scenes().size() * RiseFall::index_count
                      * MinMax::index_count),
  path_index_map_(TagMatchLess(match_crpr_clk_pin,
                               sta)),
  paths_(default_path_count_),
  sta_(sta)
{
}

bool
TagGroupBldr::empty()
{
  return path_index_map_.empty();
}

void
TagGroupBldr::init(Vertex *vertex)
{
  vertex_ = vertex;
  path_index_map_.clear();
  paths_.clear();
  has_clk_tag_ = false;
  has_genclk_src_tag_ = false;
  has_filter_tag_ = false;
  has_loop_tag_ = false;
}

void
TagGroupBldr::reportArrivalEntries() const
{
  pathIndexMapReport(&path_index_map_, sta_);
}

Path *
TagGroupBldr::tagMatchPath(Tag *tag)
{
  Path *match;
  size_t path_index;
  tagMatchPath(tag, match, path_index);
  return match;
}

void
TagGroupBldr::tagMatchPath(Tag *tag,
                           // Return values.
                           Path *&match,
                           size_t &path_index)
{
  // Find matching group tag.
  // Match is not necessarily equal to original tag because it
  // must only satisfy tagMatch.
  bool exists;
  findKeyValue(path_index_map_, tag, path_index, exists);
  if (exists)
    match = &paths_[path_index];
  else {
    match = nullptr;
    path_index = 0;
  }
}

Arrival
TagGroupBldr::arrival(size_t path_index) const
{
  return paths_[path_index].arrival();
}

void
TagGroupBldr::setArrival(Tag *tag,
                         const Arrival &arrival)
{
  // Find matching group tag (not necessarily equal to original tag).
  Path *match;
  size_t path_index;
  tagMatchPath(tag, match, path_index);
  setMatchPath(match, path_index, tag, arrival, nullptr, nullptr, nullptr);
}

void
TagGroupBldr::setMatchPath(Path *match,
                           size_t path_index,
                           Tag *tag,
                           Arrival arrival,
                           Path *prev_path,
                           Edge *prev_edge,
                           TimingArc *prev_arc)
{
  if (match) {
    Tag *tag_match = match->tag(sta_);
    // If the tag match exists there has to be a path map entry for it.
    if (tag_match != tag) {
      // Replace tag in arrival map.
      path_index_map_.erase(tag_match);
      path_index_map_[tag] = path_index;
    }
    paths_[path_index].init(vertex_, tag, arrival, prev_path, prev_edge, prev_arc,
                            sta_);
  }
  else
    insertPath(tag, arrival, prev_path, prev_edge, prev_arc);
}

void
TagGroupBldr::insertPath(Tag *tag,
                         Arrival arrival,
                         Path *prev_path,
                         Edge *prev_edge,
                         TimingArc *prev_arc)

{
  size_t path_index = paths_.size();
  path_index_map_[tag] = path_index;
  paths_.emplace_back(vertex_, tag, arrival, prev_path, prev_edge, prev_arc, sta_);

  if (tag->isClock())
    has_clk_tag_ = true;
  if (tag->isGenClkSrcPath())
    has_genclk_src_tag_ = true;
  if (tag->isFilter() || tag->clkInfo()->crprPathRefsFilter())
    has_filter_tag_ = true;
  if (tag->isLoop())
    has_loop_tag_ = true;
  if (tag->clkInfo()->isPropagated())
    has_propagated_clk_ = true;
}

void
TagGroupBldr::insertPath(const Path &path)
{
  insertPath(path.tag(sta_), path.arrival(), path.prevPath(), path.prevEdge(sta_),
             path.prevArc(sta_));
}

TagGroup *
TagGroupBldr::makeTagGroup(TagGroupIndex index,
                           const StaState *sta)
{
  return new TagGroup(index, makePathIndexMap(sta), has_clk_tag_,
                      has_genclk_src_tag_, has_filter_tag_, has_loop_tag_, sta);
}

PathIndexMap *
TagGroupBldr::makePathIndexMap(const StaState *sta)
{
  PathIndexMap *path_index_map = new PathIndexMap(TagMatchLess(true, sta));
  size_t path_index = 0;
  for (auto const [tag, path_index1] : path_index_map_) {
    (*path_index_map)[tag] = path_index;
    path_index++;
  }
  return path_index_map;
}

void
TagGroupBldr::copyPaths(TagGroup *tag_group,
                        Path *paths)
{
  for (auto const [tag1, path_index1] : path_index_map_) {
    size_t path_index2;
    bool exists2;
    tag_group->pathIndex(tag1, path_index2, exists2);
    if (exists2)
      paths[path_index2] = paths_[path_index1];
    else
      sta_->report()->critical(1360, "tag group missing tag");
  }
}

////////////////////////////////////////////////////////////////

size_t
TagGroupHash::operator()(const TagGroup *group) const
{
  return group->hash();
}

static bool
pathIndexMapEqual(const PathIndexMap *path_index_map1,
                  const PathIndexMap *path_index_map2)
{
  if (path_index_map1->size() == path_index_map2->size()) {
    for (auto const [tag1, path_index1] : *path_index_map1) {
      if (!path_index_map2->contains(tag1))
        return false;
    }
    return true;
  }
  else
    return false;
}

bool
TagGroupEqual::operator()(const TagGroup *tag_group1,
                          const TagGroup *tag_group2) const
{
  return tag_group1 == tag_group2
      || (tag_group1->hash() == tag_group2->hash()
          && pathIndexMapEqual(tag_group1->pathIndexMap(),
                               tag_group2->pathIndexMap()));
}

}  // namespace sta
