/******************************************************************************
 * Top contributors (to current version):
 *   Andrew Reynolds, Mathias Preiner, Aina Niemetz
 *
 * This file is part of the cvc5 project.
 *
 * Copyright (c) 2009-2025 by the authors listed in the file AUTHORS
 * in the top-level source directory and their institutional affiliations.
 * All rights reserved.  See the file COPYING in the top-level source
 * directory for licensing information.
 * ****************************************************************************
 *
 * Utility for processing single invocation synthesis conjectures.
 */
#include "theory/quantifiers/single_inv_partition.h"

#include <sstream>

#include "expr/node_algorithm.h"
#include "expr/skolem_manager.h"
#include "theory/quantifiers/term_util.h"

using namespace cvc5::internal;
using namespace cvc5::internal::kind;
using namespace std;

namespace cvc5::internal {
namespace theory {
namespace quantifiers {

SingleInvocationPartition::SingleInvocationPartition(Env& env)
    : EnvObj(env), d_has_input_funcs(false)
{
}

bool SingleInvocationPartition::init(Node n)
{
  // first, get types of arguments for functions
  std::vector<TypeNode> typs;
  std::map<Node, bool> visited;
  std::vector<Node> funcs;
  if (inferArgTypes(n, typs, visited))
  {
    return init(funcs, typs, n, false);
  }
  else
  {
    Trace("si-prt") << "Could not infer argument types." << std::endl;
    return false;
  }
}

Node SingleInvocationPartition::getFirstOrderVariableForFunction(Node f) const
{
  std::map<Node, Node>::const_iterator it = d_func_fo_var.find(f);
  if (it != d_func_fo_var.end())
  {
    return it->second;
  }
  return Node::null();
}

Node SingleInvocationPartition::getFunctionForFirstOrderVariable(Node v) const
{
  std::map<Node, Node>::const_iterator it = d_fo_var_to_func.find(v);
  if (it != d_fo_var_to_func.end())
  {
    return it->second;
  }
  return Node::null();
}

Node SingleInvocationPartition::getFunctionInvocationFor(Node f) const
{
  std::map<Node, Node>::const_iterator it = d_func_inv.find(f);
  if (it != d_func_inv.end())
  {
    return it->second;
  }
  return Node::null();
}

void SingleInvocationPartition::getFunctionVariables(
    std::vector<Node>& fvars) const
{
  fvars.insert(fvars.end(), d_func_vars.begin(), d_func_vars.end());
}

void SingleInvocationPartition::getFunctions(std::vector<Node>& fs) const
{
  fs.insert(fs.end(), d_all_funcs.begin(), d_all_funcs.end());
}

void SingleInvocationPartition::getSingleInvocationVariables(
    std::vector<Node>& sivars) const
{
  sivars.insert(sivars.end(), d_si_vars.begin(), d_si_vars.end());
}

void SingleInvocationPartition::getAllVariables(std::vector<Node>& vars) const
{
  vars.insert(vars.end(), d_all_vars.begin(), d_all_vars.end());
}

// gets the argument type list for the first APPLY_UF we see
bool SingleInvocationPartition::inferArgTypes(Node n,
                                              std::vector<TypeNode>& typs,
                                              std::map<Node, bool>& visited)
{
  if (visited.find(n) == visited.end())
  {
    visited[n] = true;
    if (n.getKind() != Kind::FORALL)
    {
      if (n.getKind() == Kind::APPLY_UF)
      {
        for (unsigned i = 0; i < n.getNumChildren(); i++)
        {
          typs.push_back(n[i].getType());
        }
        return true;
      }
      else
      {
        for (unsigned i = 0; i < n.getNumChildren(); i++)
        {
          if (inferArgTypes(n[i], typs, visited))
          {
            return true;
          }
        }
      }
    }
  }
  return false;
}

bool SingleInvocationPartition::init(std::vector<Node>& funcs, Node n)
{
  Trace("si-prt") << "Initialize with " << funcs.size() << " input functions ("
                  << funcs << ")..." << std::endl;
  std::vector<TypeNode> typs;
  if (!funcs.empty())
  {
    TypeNode tn0 = funcs[0].getType();
    if (tn0.isFunction())
    {
      for (unsigned i = 0, nargs = tn0.getNumChildren() - 1; i < nargs; i++)
      {
        typs.push_back(tn0[i]);
      }
    }
    for (unsigned i = 1, size = funcs.size(); i < size; i++)
    {
      TypeNode tni = funcs[i].getType();
      if (tni.getNumChildren() != tn0.getNumChildren())
      {
        // can't anti-skolemize functions of different sort
        Trace("si-prt") << "...type mismatch" << std::endl;
        return false;
      }
      else if (tni.isFunction())
      {
        for (unsigned j = 0, nargs = tni.getNumChildren() - 1; j < nargs; j++)
        {
          if (tni[j] != typs[j])
          {
            Trace("si-prt") << "...argument type mismatch" << std::endl;
            return false;
          }
        }
      }
    }
  }
  Trace("si-prt") << "#types = " << typs.size() << std::endl;
  return init(funcs, typs, n, true);
}

bool SingleInvocationPartition::init(std::vector<Node>& funcs,
                                     std::vector<TypeNode>& typs,
                                     Node n,
                                     bool has_funcs)
{
  Assert(d_arg_types.empty());
  Assert(d_input_funcs.empty());
  Assert(d_si_vars.empty());
  NodeManager* nm = nodeManager();
  d_has_input_funcs = has_funcs;
  d_arg_types.insert(d_arg_types.end(), typs.begin(), typs.end());
  d_input_funcs.insert(d_input_funcs.end(), funcs.begin(), funcs.end());
  Trace("si-prt") << "Initialize..." << std::endl;
  for (unsigned j = 0; j < d_arg_types.size(); j++)
  {
    std::stringstream ss;
    ss << "s_" << j;
    Node si_v = NodeManager::mkBoundVar(ss.str(), d_arg_types[j]);
    d_si_vars.push_back(si_v);
  }
  Assert(d_si_vars.size() == d_arg_types.size());
  for (const Node& inf : d_input_funcs)
  {
    Node sk = NodeManager::mkDummySkolem("_sik", inf.getType());
    d_input_func_sks.push_back(sk);
  }
  Trace("si-prt") << "SingleInvocationPartition::process " << n << std::endl;
  Trace("si-prt") << "Get conjuncts..." << std::endl;
  std::vector<Node> conj;
  if (collectConjuncts(n, true, conj))
  {
    Trace("si-prt") << "...success." << std::endl;
    for (unsigned i = 0; i < conj.size(); i++)
    {
      std::vector<Node> si_terms;
      std::vector<Node> si_subs;
      Trace("si-prt") << "Process conjunct : " << conj[i] << std::endl;
      // do DER on conjunct
      // Must avoid eliminating the first-order input functions in the
      // getQuantSimplify step below. We use a substitution to avoid this.
      // This makes it so that e.g. the synthesis conjecture:
      //   exists f. f!=0 ^ P
      // is not rewritten to exists f. (f=0 => false) ^ P and subsquently
      // rewritten to exists f. false ^ P by the elimination f -> 0.
      Node cr = conj[i].substitute(d_input_funcs.begin(),
                                   d_input_funcs.end(),
                                   d_input_func_sks.begin(),
                                   d_input_func_sks.end());
      cr = getQuantSimplify(cr);
      cr = cr.substitute(d_input_func_sks.begin(),
                         d_input_func_sks.end(),
                         d_input_funcs.begin(),
                         d_input_funcs.end());
      if (cr != conj[i])
      {
        Trace("si-prt-debug") << "...rewritten to " << cr << std::endl;
      }
      std::map<Node, bool> visited;
      // functions to arguments
      std::vector<Node> args;
      Subs sb;
      bool singleInvocation = true;
      bool ngroundSingleInvocation = false;
      if (processConjunct(cr, visited, args, sb))
      {
        for (size_t j = 0, vsize = sb.d_vars.size(); j < vsize; j++)
        {
          Node s = sb.d_subs[j];
          si_terms.push_back(s);
          Node op = s.hasOperator() ? s.getOperator() : s;
          Assert(d_func_fo_var.find(op) != d_func_fo_var.end());
          si_subs.push_back(d_func_fo_var[op]);
        }
        std::map<Node, Node> subs_map;
        std::map<Node, Node> subs_map_rev;
        // normalize the invocations
        if (!sb.empty())
        {
          cr = sb.apply(cr);
        }
        std::vector<Node> children;
        children.push_back(cr);
        sb.clear();
        Trace("si-prt") << "...single invocation, with arguments: "
                        << std::endl;
        for (unsigned j = 0; j < args.size(); j++)
        {
          Trace("si-prt") << args[j] << " ";
          if (args[j].getKind() == Kind::BOUND_VARIABLE
              && !sb.contains(args[j]))
          {
            sb.add(args[j], d_si_vars[j]);
          }
          else
          {
            children.push_back(d_si_vars[j].eqNode(args[j]).negate());
          }
        }
        Trace("si-prt") << std::endl;
        cr = nm->mkOr(children);
        cr = sb.apply(cr);
        Trace("si-prt-debug") << "...normalized invocations to " << cr
                              << std::endl;
        // now must check if it has other bound variables
        std::unordered_set<Node> fvs;
        expr::getFreeVariables(cr, fvs);
        // bound variables must be contained in the single invocation variables
        for (const Node& bv : fvs)
        {
          if (std::find(d_si_vars.begin(), d_si_vars.end(), bv)
              == d_si_vars.end())
          {
            // getFreeVariables also collects functions in the rare case that
            // we are synthesizing a function with 0 arguments, take this into
            // account here.
            if (std::find(d_input_funcs.begin(), d_input_funcs.end(), bv)
                == d_input_funcs.end())
            {
              Trace("si-prt")
                  << "...not ground single invocation." << std::endl;
              ngroundSingleInvocation = true;
              singleInvocation = false;
            }
          }
        }
        if (singleInvocation)
        {
          Trace("si-prt") << "...ground single invocation" << std::endl;
        }
      }
      else
      {
        Trace("si-prt") << "...not single invocation." << std::endl;
        singleInvocation = false;
        // rename bound variables with maximal overlap with si_vars
        std::unordered_set<Node> fvs;
        expr::getFreeVariables(cr, fvs);
        std::vector<Node> termsNs;
        std::vector<Node> subsNs;
        for (const Node& v : fvs)
        {
          TypeNode tn = v.getType();
          Trace("si-prt-debug")
              << "Fit bound var: " << v << " with si." << std::endl;
          for (unsigned k = 0; k < d_si_vars.size(); k++)
          {
            if (tn == d_arg_types[k])
            {
              if (std::find(subsNs.begin(), subsNs.end(), d_si_vars[k])
                  == subsNs.end())
              {
                termsNs.push_back(v);
                subsNs.push_back(d_si_vars[k]);
                Trace("si-prt-debug") << "  ...use " << d_si_vars[k]
                                      << std::endl;
                break;
              }
            }
          }
        }
        Assert(termsNs.size() == subsNs.size());
        cr = cr.substitute(
            termsNs.begin(), termsNs.end(), subsNs.begin(), subsNs.end());
      }
      Trace("si-prt") << ".....got si=" << singleInvocation
                      << ", result : " << cr << std::endl;
      d_conjuncts[2].push_back(cr);
      std::unordered_set<Node> fvs;
      expr::getFreeVariables(cr, fvs);
      d_all_vars.insert(fvs.begin(), fvs.end());
      if (singleInvocation)
      {
        // replace with single invocation formulation
        Assert(si_terms.size() == si_subs.size());
        cr = cr.substitute(
            si_terms.begin(), si_terms.end(), si_subs.begin(), si_subs.end());
        Trace("si-prt") << ".....si version=" << cr << std::endl;
        d_conjuncts[0].push_back(cr);
      }
      else
      {
        d_conjuncts[1].push_back(cr);
        if (ngroundSingleInvocation)
        {
          d_conjuncts[3].push_back(cr);
        }
      }
    }
  }
  else
  {
    Trace("si-prt") << "...failed." << std::endl;
    return false;
  }
  return true;
}

bool SingleInvocationPartition::collectConjuncts(Node n,
                                                 bool pol,
                                                 std::vector<Node>& conj)
{
  if ((!pol && n.getKind() == Kind::OR) || (pol && n.getKind() == Kind::AND))
  {
    for (unsigned i = 0; i < n.getNumChildren(); i++)
    {
      if (!collectConjuncts(n[i], pol, conj))
      {
        return false;
      }
    }
  }
  else if (n.getKind() == Kind::NOT)
  {
    return collectConjuncts(n[0], !pol, conj);
  }
  else if (n.getKind() == Kind::FORALL)
  {
    return false;
  }
  else
  {
    if (!pol)
    {
      n = TermUtil::simpleNegate(n);
    }
    Trace("si-prt") << "Conjunct : " << n << std::endl;
    conj.push_back(n);
  }
  return true;
}

bool SingleInvocationPartition::processConjunct(Node n,
                                                std::map<Node, bool>& visited,
                                                std::vector<Node>& args,
                                                Subs& sb)
{
  std::map<Node, bool>::iterator it = visited.find(n);
  if (it != visited.end())
  {
    return true;
  }
  else
  {
    bool ret = true;
    for (unsigned i = 0; i < n.getNumChildren(); i++)
    {
      if (!processConjunct(n[i], visited, args, sb))
      {
        ret = false;
      }
    }
    if (ret)
    {
      Node f;
      bool success = false;
      if (d_has_input_funcs)
      {
        f = n.hasOperator() ? n.getOperator() : n;
        if (std::find(d_input_funcs.begin(), d_input_funcs.end(), f)
            != d_input_funcs.end())
        {
          // If n is an application of a function-to-synthesize f, or is
          // itself a function-to-synthesize, then n must be fully applied.
          // This catches cases where n is a function-to-synthesize that occurs
          // in a higher-order context.
          // If the type of n is functional, then it is not fully applied.
          if (n.getType().isFunction())
          {
            ret = false;
            success = false;
          }
          else
          {
            success = true;
          }
        }
      }
      else
      {
        if (n.getKind() == Kind::APPLY_UF)
        {
          f = n.getOperator();
          success = true;
        }
      }
      if (success)
      {
        Trace("si-prt-debug") << "Process " << n << std::endl;
        if (!sb.contains(n))
        {
          // check if it matches the type requirement
          if (isAntiSkolemizableType(f))
          {
            if (args.empty())
            {
              // record arguments
              for (unsigned i = 0; i < n.getNumChildren(); i++)
              {
                args.push_back(n[i]);
              }
            }
            else
            {
              // arguments must be the same as those already recorded
              for (unsigned i = 0; i < n.getNumChildren(); i++)
              {
                if (args[i] != n[i])
                {
                  Trace("si-prt-debug") << "...bad invocation : " << n
                                        << " at arg " << i << "." << std::endl;
                  ret = false;
                  break;
                }
              }
            }
            if (ret)
            {
              sb.add(n, d_func_inv[f]);
            }
          }
          else
          {
            Trace("si-prt-debug") << "... " << f << " is a bad operator."
                                  << std::endl;
            ret = false;
          }
        }
      }
    }
    visited[n] = ret;
    return ret;
  }
}

bool SingleInvocationPartition::isAntiSkolemizableType(Node f)
{
  std::map<Node, bool>::iterator it = d_funcs.find(f);
  if (it != d_funcs.end())
  {
    return it->second;
  }
  else
  {
    TypeNode tn = f.getType();
    bool ret = false;
    if (((tn.isFunction() && tn.getNumChildren() == d_arg_types.size() + 1)
         || (d_arg_types.empty() && tn.getNumChildren() == 0)))
    {
      ret = true;
      std::vector<Node> children;
      children.push_back(f);
      // TODO: permutations of arguments
      for (unsigned i = 0; i < d_arg_types.size(); i++)
      {
        children.push_back(d_si_vars[i]);
        if (tn[i] != d_arg_types[i])
        {
          ret = false;
          break;
        }
      }
      if (ret)
      {
        Node t;
        if (children.size() > 1)
        {
          t = nodeManager()->mkNode(Kind::APPLY_UF, children);
        }
        else
        {
          t = children[0];
        }
        d_func_inv[f] = t;
        std::stringstream ss;
        ss << "F_" << f;
        TypeNode rt;
        if (d_arg_types.empty())
        {
          rt = tn;
        }
        else
        {
          rt = tn.getRangeType();
        }
        Node v = nodeManager()->mkBoundVar(ss.str(), rt);
        d_func_fo_var[f] = v;
        d_fo_var_to_func[v] = f;
        d_func_vars.push_back(v);
        d_all_funcs.push_back(f);
      }
    }
    d_funcs[f] = ret;
    return ret;
  }
}

Node SingleInvocationPartition::getConjunct(int index)
{
  return d_conjuncts[index].empty()
             ? nodeManager()->mkConst(true)
             : (d_conjuncts[index].size() == 1
                    ? d_conjuncts[index][0]
                    : nodeManager()->mkNode(Kind::AND, d_conjuncts[index]));
}

void SingleInvocationPartition::debugPrint(const char* c)
{
  Trace(c) << "Single invocation variables : ";
  for (unsigned i = 0; i < d_si_vars.size(); i++)
  {
    Trace(c) << d_si_vars[i] << " ";
  }
  Trace(c) << std::endl;
  Trace(c) << "Functions : " << std::endl;
  for (std::map<Node, bool>::iterator it = d_funcs.begin(); it != d_funcs.end();
       ++it)
  {
    Trace(c) << "  " << it->first << " : ";
    if (it->second)
    {
      Trace(c) << d_func_inv[it->first] << " " << d_func_fo_var[it->first]
               << std::endl;
    }
    else
    {
      Trace(c) << "not incorporated." << std::endl;
    }
  }
  for (unsigned i = 0; i < 4; i++)
  {
    Trace(c) << (i == 0 ? "Single invocation"
                        : (i == 1 ? "Non-single invocation"
                                  : (i == 2 ? "All"
                                            : "Non-ground single invocation")));
    Trace(c) << " conjuncts: " << std::endl;
    for (unsigned j = 0; j < d_conjuncts[i].size(); j++)
    {
      Trace(c) << "  " << (j + 1) << " : " << d_conjuncts[i][j] << std::endl;
    }
  }
  Trace(c) << std::endl;
}

Node SingleInvocationPartition::getQuantSimplify(TNode n) const
{
  std::unordered_set<Node> fvs;
  expr::getFreeVariables(n, fvs);
  if (fvs.empty())
  {
    return rewrite(n);
  }
  std::vector<Node> bvs(fvs.begin(), fvs.end());
  NodeManager* nm = nodeManager();
  Node q = nm->mkNode(Kind::FORALL, nm->mkNode(Kind::BOUND_VAR_LIST, bvs), n);
  q = rewrite(q);
  return TermUtil::getRemoveQuantifiers(q);
}

}  // namespace quantifiers
}  // namespace theory
}  // namespace cvc5::internal
