// Copyright lowRISC contributors.
// Licensed under the Apache License, Version 2.0, see LICENSE for details.
// SPDX-License-Identifier: Apache-2.0

#include "sv_scoped.h"

#include <cassert>
#include <sstream>

// Set scope by name, returning the old scope. If the name doesn't describe a
// valid scope, throw an SVScoped::Error.
static svScope SetAbsScope(const std::string &name) {
  svScope new_scope = svGetScopeFromName(name.c_str());
  if (!new_scope)
    throw SVScoped::Error(name);
  return svSetScope(new_scope);
}

// Resolve name to a scope, using the rules described in the comment above the
// class in sv_scoped.h, and then set it. Returns the old scope.
static svScope SetRelScope(const std::string &name) {
  // Absolute (or empty) names resolve to themselves
  if (name[0] != '.') {
    return SetAbsScope(name);
  }

  svScope prev_scope = svGetScope();

  // Special case: If name is ".", it means to use the current scope. Rather
  // than changing scope, we can just return where we are going to stay.
  if (name == ".")
    return prev_scope;

  // For anything else, count how many dots appear after the first one (so
  // ..foo gives an up_count of 1; ...bar gives an up_count of 2).
  size_t first_not_dot = name.find_first_not_of('.', 1);
  if (first_not_dot == std::string::npos) {
    // name looks like "....": that's fine, it just means to go up some number
    // of steps from the current position and not down again. Amend
    // first_not_dot to point at the '\0'.
    first_not_dot = name.size();
  }
  size_t up_count = first_not_dot - 1;

  // Get the name of the current scope, so that we can perform surgery.
  std::string scope_name = svGetNameFromScope(prev_scope);

  // scope_name will look something like "TOP.foo.bar". Search up_count
  // dots from the end, setting last_dot to point at the last dot that should
  // appear in the resolved name.
  //
  // If up_count is too large, behave like "cd /; cd .." and stop at the
  // left-most dot.
  size_t last_dot = scope_name.size();
  for (size_t i = 0; i < up_count; ++i) {
    // This shouldn't ever trigger (because it would mean scope_name was
    // either empty or started with a "."), but allowing it makes the code a
    // bit more uniform.
    if (last_dot == 0)
      break;

    size_t dot = scope_name.rfind('.', last_dot - 1);
    if (dot == std::string::npos)
      break;

    last_dot = dot;
  }

  // Delete everything from last_dot onwards. If we are actually pointing at a
  // dot, this will do something like "TOP.foo.bar" -> "TOP.foo". If up_count
  // was zero or there were no dots, last_dot will equal the size of the string
  // (which means, conveniently, that erase is a no-op).
  scope_name.erase(last_dot);

  // If first_not_dot points inside name (so name looked like "..foo.bar"
  // rather than "..."), subtract one to point at the last dot of the initial
  // segment (we know there is one because name[0] == '.') and then append
  // everything to scope_name starting from there. For example, if scope_name
  // was "TOP.foo.bar.baz" and name was "..qux", we will have just amended
  // scope_name to be "TOP.foo.bar". Now we want to add ".qux".
  if (first_not_dot < name.size()) {
    scope_name.append(name, first_not_dot - 1, std::string::npos);
  }

  return SetAbsScope(scope_name);
}

SVScoped::SVScoped(const std::string &name) : prev_scope_(SetRelScope(name)) {}

SVScoped::Error::Error(const std::string &scope_name)
    : scope_name_(scope_name) {
  std::ostringstream oss;
  oss << "No such SystemVerilog scope: `" << scope_name << "'.";
  msg_ = oss.str();
}

std::string SVScoped::join_sv_scopes(const std::string &a,
                                     const std::string &b) {
  assert(a.size() && b.size());
  // If a = ".." and b = "foo.bar", we want "..foo.bar". Otherwise, a
  // = "..foo" and b = "bar.baz", so we want "..foo.bar.baz"
  // (inserting a "." between the two)
  return (a.back() == '.') ? a + b : a + "." + b;
}
