/* -*- mode: C++; c-basic-offset: 2; indent-tabs-mode: nil -*- */

/*
 *  Main authors:
 *     Guido Tack <guido.tack@monash.edu>
 */

/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#include <minizinc/astiterator.hh>
#include <minizinc/output.hh>
#include <minizinc/typecheck.hh>

namespace MiniZinc {

namespace {

// Test if all parameters and the return type are par
bool is_completely_par(EnvI& env, FunctionI* fi, const std::vector<Type>& tv) {
  if (fi->e() != nullptr) {
    // This is not a builtin, so check parameters
    for (auto* p : fi->params()) {
      if (p->type().isvar()) {
        return false;
      }
    }
  }
  return fi->rtype(env, tv, false).isPar();
}

}  // namespace

void check_output_par_fn(EnvI& env, Call* rhs) {
  std::vector<Type> tv(rhs->argCount());
  for (unsigned int i = rhs->argCount(); (i--) != 0U;) {
    tv[i] = rhs->arg(i)->type();
    tv[i].ti(Type::TI_PAR);
  }
  FunctionI* decl = env.output->matchFn(env, rhs->id(), tv, false);
  if (decl == nullptr) {
    FunctionI* origdecl = env.model->matchFn(env, rhs->id(), tv, false);
    if (origdecl == nullptr || !is_completely_par(env, origdecl, tv)) {
      std::ostringstream ss;
      ss << "function " << rhs->id() << " is used in output, par version needed";
      throw FlatteningError(env, rhs->loc(), ss.str());
    }
    if (!origdecl->fromStdLib()) {
      decl = copy(env, env.cmap, origdecl)->cast<FunctionI>();
      CollectOccurrencesE ce(env.outputVarOccurrences, decl);
      top_down(ce, decl->e());
      top_down(ce, decl->ti());
      for (unsigned int i = decl->params().size(); (i--) != 0U;) {
        top_down(ce, decl->params()[i]);
      }
      (void)env.output->registerFn(env, decl, true);
      env.output->addItem(decl);
    } else {
      decl = origdecl;
    }
  }
  rhs->type(decl->rtype(env, tv, false));
  rhs->decl(decl);
}

bool cannot_use_rhs_for_output(EnvI& env, Expression* e,
                               std::unordered_set<FunctionI*>& seen_functions) {
  if (e == nullptr) {
    return true;
  }

  class V : public EVisitor {
  public:
    EnvI& env;
    std::unordered_set<FunctionI*>& seenFunctions;
    bool success;
    V(EnvI& env0, std::unordered_set<FunctionI*>& seenFunctions0)
        : env(env0), seenFunctions(seenFunctions0), success(true) {}
    /// Visit anonymous variable
    void vAnonVar(const AnonVar& /*v*/) { success = false; }
    /// Visit array literal
    void vArrayLit(const ArrayLit& /*al*/) {}
    /// Visit array access
    void vArrayAccess(const ArrayAccess& /*aa*/) {}
    /// Visit array comprehension
    void vComprehension(const Comprehension& /*c*/) {}
    /// Visit if-then-else
    void vITE(const ITE& /*ite*/) {}
    /// Visit binary operator
    void vBinOp(const BinOp& /*bo*/) {}
    /// Visit unary operator
    void vUnOp(const UnOp& /*uo*/) {}
    /// Visit call
    void vCall(Call& c) {
      std::vector<Type> tv(c.argCount());
      for (unsigned int i = c.argCount(); (i--) != 0U;) {
        tv[i] = c.arg(i)->type();
        tv[i].ti(Type::TI_PAR);
      }
      FunctionI* decl = env.output->matchFn(env, c.id(), tv, false);
      Type t;
      if (decl == nullptr) {
        FunctionI* origdecl = env.model->matchFn(env, c.id(), tv, false);
        if (origdecl == nullptr) {
          std::ostringstream ss;
          ss << "function " << c.id() << " is used in output, par version needed";
          throw FlatteningError(env, c.loc(), ss.str());
        }
        bool seen = (seenFunctions.find(origdecl) != seenFunctions.end());
        if (seen) {
          success = false;
        } else {
          seenFunctions.insert(origdecl);
          if ((origdecl->e() != nullptr) &&
              cannot_use_rhs_for_output(env, origdecl->e(), seenFunctions)) {
            success = false;
          } else {
            if (!origdecl->fromStdLib()) {
              decl = copy(env, env.cmap, origdecl)->cast<FunctionI>();
              CollectOccurrencesE ce(env.outputVarOccurrences, decl);
              top_down(ce, decl->e());
              top_down(ce, decl->ti());
              for (unsigned int i = decl->params().size(); (i--) != 0U;) {
                top_down(ce, decl->params()[i]);
              }
              (void)env.output->registerFn(env, decl, true);
              env.output->addItem(decl);
              output_vardecls(env, origdecl, decl->e());
              output_vardecls(env, origdecl, decl->ti());
            } else {
              decl = origdecl;
            }
            c.decl(decl);
          }
        }
      }
      if (success) {
        t = decl->rtype(env, tv, false);
        if (!t.isPar()) {
          success = false;
        }
      }
    }
    void vId(const Id& /*id*/) {}
    /// Visit let
    void vLet(const Let& /*let*/) { success = false; }
    /// Visit variable declaration
    void vVarDecl(const VarDecl& /*vd*/) {}
    /// Visit type inst
    void vTypeInst(const TypeInst& /*ti*/) {}
    /// Visit TIId
    void vTIId(const TIId& /*tiid*/) {}
    /// Determine whether to enter node
    bool enter(Expression* /*e*/) const { return success; }
  } _v(env, seen_functions);
  top_down(_v, e);

  return !_v.success;
}

bool cannot_use_rhs_for_output(EnvI& env, Expression* e) {
  std::unordered_set<FunctionI*> seen_functions;
  return cannot_use_rhs_for_output(env, e, seen_functions);
}

void remove_is_output(VarDecl* vd) {
  if (vd == nullptr) {
    return;
  }
  vd->ann().remove(constants().ann.output_var);
  vd->ann().removeCall(constants().ann.output_array);
}

void copy_output(EnvI& e) {
  struct CopyOutput : public EVisitor {
    EnvI& env;
    CopyOutput(EnvI& env0) : env(env0) {}
    static void vId(Id& _id) { _id.decl(_id.decl()->flat()); }
    void vCall(Call& c) {
      std::vector<Type> tv(c.argCount());
      for (unsigned int i = c.argCount(); (i--) != 0U;) {
        tv[i] = c.arg(i)->type();
        tv[i].ti(Type::TI_PAR);
      }
      FunctionI* decl = c.decl();
      if (!decl->fromStdLib()) {
        env.flatAddItem(decl);
      }
    }
  };

  if (OutputI* oi = e.model->outputItem()) {
    GCLock lock;
    auto* noi = copy(e, oi)->cast<OutputI>();
    CopyOutput co(e);
    top_down(co, noi->e());
    e.flatAddItem(noi);
  }
}

void cleanup_output(EnvI& env) {
  for (auto& i : *env.output) {
    if (auto* vdi = i->dynamicCast<VarDeclI>()) {
      vdi->e()->flat(nullptr);
    }
  }
}

void make_par(EnvI& env, Expression* e) {
  class OutputJSON : public EVisitor {
  public:
    EnvI& env;
    OutputJSON(EnvI& env0) : env(env0) {}
    void vCall(Call& c) {
      if (c.id() == "outputJSON") {
        bool outputObjective = (c.argCount() == 1 && eval_bool(env, c.arg(0)));
        c.id(ASTString("array1d"));
        Expression* json =
            copy(env, env.cmap, create__json_output(env, outputObjective, false, false));
        std::vector<Expression*> new_args({json});
        new_args[0]->type(Type::parstring(1));
        c.args(new_args);
      }
    }
  } _outputJSON(env);
  top_down(_outputJSON, e);
  class Par : public EVisitor {
  public:
    /// Visit variable declaration
    static void vVarDecl(VarDecl& vd) { vd.ti()->type(vd.type()); }
    /// Determine whether to enter node
    static bool enter(Expression* e) {
      Type t = e->type();
      t.ti(Type::TI_PAR);
      t.cv(false);
      e->type(t);
      return true;
    }
  } _par;
  top_down(_par, e);
  class Decls : public EVisitor {
  public:
    EnvI& env;
    Decls(EnvI& env0) : env(env0) {}
    void vCall(Call& c) {
      if (c.id() == "format" || c.id() == "show" || c.id() == "showDzn" || c.id() == "showJSON") {
        unsigned int enumId = c.arg(c.argCount() - 1)->type().enumId();
        if (enumId != 0U && c.arg(c.argCount() - 1)->type().dim() != 0) {
          const std::vector<unsigned int>& enumIds = env.getArrayEnum(enumId);
          enumId = enumIds[enumIds.size() - 1];
        }
        if (enumId > 0) {
          GCLock lock;
          Expression* obj = c.arg(c.argCount() - 1);
          Id* ti_id = env.getEnum(enumId)->e()->id();
          std::string enumName = create_enum_to_string_name(ti_id, "_toString_");
          bool is_json = c.id() == "showJSON";
          const int dimensions = obj->type().dim();
          if (is_json && dimensions > 1) {
            // Create generators for dimensions selection
            std::vector<Expression*> slice_dimensions(dimensions);
            std::vector<Generator> generators;
            generators.reserve(dimensions - 1);
            auto* idx_ti = new TypeInst(Location().introduce(), Type::parint());
            for (int i = 0; i < dimensions - 1; ++i) {
              auto* idx_i = new VarDecl(Location().introduce(), idx_ti, env.genId());
              idx_i->toplevel(false);
              Call* index_set_xx = new Call(
                  Location().introduce(),
                  "index_set_" + std::to_string(i + 1) + "of" + std::to_string(dimensions), {obj});
              index_set_xx->type(Type::parsetint());
              generators.push_back(Generator({idx_i}, index_set_xx, nullptr));
              slice_dimensions[i] =
                  new BinOp(Location().introduce(), idx_i->id(), BOT_DOTDOT, idx_i->id());
              slice_dimensions[i]->type(Type::parsetint());
            }

            // Construct innermost slicing operation
            Call* index_set_n = new Call(
                Location().introduce(),
                "index_set_" + std::to_string(dimensions) + "of" + std::to_string(dimensions),
                {obj});
            index_set_n->type(Type::parsetint());
            slice_dimensions[dimensions - 1] = index_set_n;
            auto* al_slice_dim = new ArrayLit(Location().introduce(), slice_dimensions);
            al_slice_dim->type(Type::parsetint(1));

            auto* slice_call =
                new Call(Location().introduce(), "slice_1d", {obj, al_slice_dim, index_set_n});
            Type tt = obj->type();
            tt.dim(1);
            slice_call->type(tt);
            Call* _toString_ENUM =
                new Call(Location().introduce(), enumName,
                         {slice_call, constants().boollit(false), constants().boollit(true)});
            _toString_ENUM->type(Type::parstring());

            // Build multi-level JSON Array string
            auto* comma = new StringLit(Location().introduce(), ", ");
            comma->type(Type::parstring());
            auto join = [&](Expression* expr, Generator gen) -> Expression* {
              Generators generators;
              generators.g.push_back(gen);
              auto* comp = new Comprehension(Location().introduce(), expr, generators, false);
              comp->type(Type::parstring(1));
              Call* cc = new Call(Location().introduce(), "join", {comma, comp});
              cc->type(Type::parstring());
              return cc;
            };
            auto* sl_open = new StringLit(Location().introduce(), "[");
            sl_open->type(Type::parstring());
            auto* sl_close = new StringLit(Location().introduce(), "]");
            sl_close->type(Type::parstring());

            auto* al_concat = new ArrayLit(
                Location().introduce(),
                std::vector<Expression*>(
                    {sl_open, join(_toString_ENUM, generators[dimensions - 2]), sl_close}));
            al_concat->type(Type::parstring(1));
            for (int i = dimensions - 3; i >= 0; --i) {
              Call* concat = new Call(Location().introduce(), "concat", {al_concat});
              concat->type(Type::parstring());
              al_concat = new ArrayLit(
                  Location().introduce(),
                  std::vector<Expression*>({sl_open, join(concat, generators[i]), sl_close}));
              al_concat->type(Type::parstring(1));
            }
            std::vector<Expression*> args = {al_concat};
            c.args(args);
            c.id(ASTString("concat"));
          } else {
            std::vector<Expression*> args = {obj, constants().boollit(c.id() == "showDzn"),
                                             constants().boollit(is_json)};
            c.args(args);
            c.id(ASTString(enumName));
          }
        }
        if (c.id() == "showDzn" || (c.id() == "showJSON" && enumId > 0)) {
          c.id(constants().ids.show);
        }
      }
      c.decl(env.model->matchFn(env, &c, false));
    }
    void vBinOp(BinOp& bo) {
      std::vector<Expression*> args = {bo.lhs(), bo.rhs()};
      bo.decl(env.model->matchFn(env, bo.opToString(), args, false));
    }
    void vUnop(UnOp& uo) {
      std::vector<Expression*> args = {uo.e()};
      uo.decl(env.model->matchFn(env, uo.opToString(), args, false));
    }
  } _decls(env);
  top_down(_decls, e);
}

void check_rename_var(EnvI& e, VarDecl* vd) {
  if (vd->id()->idn() != vd->flat()->id()->idn()) {
    auto* vd_rename_ti = copy(e, e.cmap, vd->ti())->cast<TypeInst>();
    auto* vd_rename =
        new VarDecl(Location().introduce(), vd_rename_ti, vd->flat()->id()->idn(), nullptr);
    vd_rename->flat(vd->flat());
    make_par(e, vd_rename);
    vd->e(vd_rename->id());
    e.output->addItem(new VarDeclI(Location().introduce(), vd_rename));
  }
}

class ClearAnnotations {
public:
  /// Push all elements of \a v onto \a stack
  template <class E>
  static void pushVec(std::vector<Expression*>& stack, ASTExprVec<E> v) {
    for (unsigned int i = 0; i < v.size(); i++) {
      stack.push_back(v[i]);
    }
  }

  static void run(Expression* root) {
    std::vector<Expression*> stack;
    stack.push_back(root);
    while (!stack.empty()) {
      Expression* e = stack.back();
      stack.pop_back();
      if (e == nullptr) {
        continue;
      }
      e->ann().clear();
      switch (e->eid()) {
        case Expression::E_INTLIT:
        case Expression::E_FLOATLIT:
        case Expression::E_BOOLLIT:
        case Expression::E_STRINGLIT:
        case Expression::E_ID:
        case Expression::E_ANON:
        case Expression::E_TIID:
          break;
        case Expression::E_SETLIT:
          pushVec(stack, e->template cast<SetLit>()->v());
          break;
        case Expression::E_ARRAYLIT:
          for (unsigned int i = 0; i < e->cast<ArrayLit>()->size(); i++) {
            stack.push_back((*e->cast<ArrayLit>())[i]);
          }
          break;
        case Expression::E_ARRAYACCESS:
          pushVec(stack, e->template cast<ArrayAccess>()->idx());
          stack.push_back(e->template cast<ArrayAccess>()->v());
          break;
        case Expression::E_COMP: {
          auto* comp = e->template cast<Comprehension>();
          for (unsigned int i = comp->numberOfGenerators(); (i--) != 0U;) {
            stack.push_back(comp->where(i));
            stack.push_back(comp->in(i));
            for (unsigned int j = comp->numberOfDecls(i); (j--) != 0U;) {
              stack.push_back(comp->decl(i, j));
            }
          }
          stack.push_back(comp->e());
        } break;
        case Expression::E_ITE: {
          ITE* ite = e->template cast<ITE>();
          stack.push_back(ite->elseExpr());
          for (int i = 0; i < ite->size(); i++) {
            stack.push_back(ite->ifExpr(i));
            stack.push_back(ite->thenExpr(i));
          }
        } break;
        case Expression::E_BINOP:
          stack.push_back(e->template cast<BinOp>()->rhs());
          stack.push_back(e->template cast<BinOp>()->lhs());
          break;
        case Expression::E_UNOP:
          stack.push_back(e->template cast<UnOp>()->e());
          break;
        case Expression::E_CALL:
          for (unsigned int i = 0; i < e->template cast<Call>()->argCount(); i++) {
            stack.push_back(e->template cast<Call>()->arg(i));
          }
          break;
        case Expression::E_VARDECL:
          stack.push_back(e->template cast<VarDecl>()->e());
          stack.push_back(e->template cast<VarDecl>()->ti());
          break;
        case Expression::E_LET:
          stack.push_back(e->template cast<Let>()->in());
          pushVec(stack, e->template cast<Let>()->let());
          break;
        case Expression::E_TI:
          stack.push_back(e->template cast<TypeInst>()->domain());
          pushVec(stack, e->template cast<TypeInst>()->ranges());
          break;
      }
    }
  }
};

void output_vardecls(EnvI& env, Item* ci, Expression* e) {
  class O : public EVisitor {
  public:
    EnvI& env;
    Item* ci;
    O(EnvI& env0, Item* ci0) : env(env0), ci(ci0) {}
    void vId(Id& id) {
      if (&id == constants().absent) {
        return;
      }
      if (!id.decl()->toplevel()) {
        return;
      }
      VarDecl* vd = id.decl();
      VarDecl* reallyFlat = vd->flat();
      while (reallyFlat != nullptr && reallyFlat != reallyFlat->flat()) {
        reallyFlat = reallyFlat->flat();
      }
      auto idx = reallyFlat != nullptr ? env.outputFlatVarOccurrences.idx.find(reallyFlat->id())
                                       : env.outputFlatVarOccurrences.idx.end();
      auto idx2 = env.outputVarOccurrences.idx.find(vd->id());
      if (idx == env.outputFlatVarOccurrences.idx.end() &&
          idx2 == env.outputVarOccurrences.idx.end()) {
        auto* nvi = new VarDeclI(Location().introduce(), copy(env, env.cmap, vd)->cast<VarDecl>());
        Type t = nvi->e()->ti()->type();
        if (t.ti() != Type::TI_PAR) {
          t.ti(Type::TI_PAR);
        }
        make_par(env, nvi->e());
        nvi->e()->ti()->domain(nullptr);
        nvi->e()->flat(vd->flat());
        ClearAnnotations::run(nvi->e());
        nvi->e()->introduced(false);
        if (reallyFlat != nullptr) {
          env.outputFlatVarOccurrences.addIndex(reallyFlat, static_cast<int>(env.output->size()));
        }
        env.outputVarOccurrences.addIndex(nvi, static_cast<int>(env.output->size()));
        env.outputVarOccurrences.add(nvi->e(), ci);
        env.output->addItem(nvi);

        IdMap<KeepAlive>::iterator it;
        if ((it = env.reverseMappers.find(nvi->e()->id())) != env.reverseMappers.end()) {
          Call* rhs = copy(env, env.cmap, it->second())->cast<Call>();
          check_output_par_fn(env, rhs);
          output_vardecls(env, nvi, it->second());
          nvi->e()->e(rhs);
        } else if ((reallyFlat != nullptr) && cannot_use_rhs_for_output(env, reallyFlat->e())) {
          assert(nvi->e()->flat());
          nvi->e()->e(nullptr);
          if (nvi->e()->type().dim() == 0) {
            reallyFlat->addAnnotation(constants().ann.output_var);
          } else {
            std::vector<Expression*> args(reallyFlat->e()->type().dim());
            for (unsigned int i = 0; i < args.size(); i++) {
              if (nvi->e()->ti()->ranges()[i]->domain() == nullptr) {
                args[i] = new SetLit(Location().introduce(),
                                     eval_intset(env, reallyFlat->ti()->ranges()[i]->domain()));
              } else {
                args[i] = new SetLit(Location().introduce(),
                                     eval_intset(env, nvi->e()->ti()->ranges()[i]->domain()));
              }
            }
            auto* al = new ArrayLit(Location().introduce(), args);
            args.resize(1);
            args[0] = al;
            reallyFlat->addAnnotation(
                new Call(Location().introduce(), constants().ann.output_array, args));
          }
          check_rename_var(env, nvi->e());
        } else {
          output_vardecls(env, nvi, nvi->e()->ti());
          output_vardecls(env, nvi, nvi->e()->e());
        }
        CollectOccurrencesE ce(env.outputVarOccurrences, nvi);
        top_down(ce, nvi->e());
      }
    }
  } _o(env, ci);
  top_down(_o, e);
}

void process_deletions(EnvI& e) {
  std::vector<VarDecl*> deletedVarDecls;
  for (unsigned int i = 0; i < e.output->size(); i++) {
    if (auto* vdi = (*e.output)[i]->dynamicCast<VarDeclI>()) {
      if (!vdi->removed() && e.outputVarOccurrences.occurrences(vdi->e()) == 0 &&
          !vdi->e()->ann().contains(constants().ann.mzn_check_var) &&
          !(vdi->e()->id()->idn() == -1 && (vdi->e()->id()->v() == "_mzn_solution_checker" ||
                                            vdi->e()->id()->v() == "_mzn_stats_checker"))) {
        CollectDecls cd(e.outputVarOccurrences, deletedVarDecls, vdi);
        top_down(cd, vdi->e()->e());
        remove_is_output(vdi->e()->flat());
        if (e.outputVarOccurrences.find(vdi->e()) != -1) {
          e.outputVarOccurrences.remove(vdi->e());
        }
        vdi->remove();
      }
    }
  }
  while (!deletedVarDecls.empty()) {
    VarDecl* cur = deletedVarDecls.back();
    deletedVarDecls.pop_back();
    if (e.outputVarOccurrences.occurrences(cur) == 0) {
      auto cur_idx = e.outputVarOccurrences.idx.find(cur->id());
      if (cur_idx != e.outputVarOccurrences.idx.end()) {
        auto* vdi = (*e.output)[cur_idx->second]->cast<VarDeclI>();
        if (!vdi->removed()) {
          CollectDecls cd(e.outputVarOccurrences, deletedVarDecls, vdi);
          top_down(cd, cur->e());
          remove_is_output(vdi->e()->flat());
          if (e.outputVarOccurrences.find(vdi->e()) != -1) {
            e.outputVarOccurrences.remove(vdi->e());
          }
          vdi->remove();
        }
      }
    }
  }

  for (auto& it : e.outputVarOccurrences.itemMap) {
    std::vector<Item*> toRemove;
    for (auto* iit : it.second) {
      if (iit->removed()) {
        toRemove.push_back(iit);
      }
    }
    for (auto& i : toRemove) {
      it.second.erase(i);
    }
  }
}

void create_dzn_output_item(EnvI& e, bool outputObjective, bool includeOutputItem, bool hasChecker,
                            bool outputForChecker) {
  std::vector<Expression*> outputVars;

  class DZNOVisitor : public ItemVisitor {
  protected:
    EnvI& _e;
    bool _outputObjective;
    bool _includeOutputItem;
    bool _outputForChecker;
    std::vector<Expression*>& _outputVars;
    bool _hadAddToOutput;

  public:
    DZNOVisitor(EnvI& e, bool outputObjective, bool includeOutputItem, bool outputForChecker,
                std::vector<Expression*>& outputVars)
        : _e(e),
          _outputObjective(outputObjective),
          _includeOutputItem(includeOutputItem),
          _outputForChecker(outputForChecker),
          _outputVars(outputVars),
          _hadAddToOutput(false) {}
    void vVarDeclI(VarDeclI* vdi) {
      VarDecl* vd = vdi->e();
      bool process_var = false;
      if (_outputForChecker) {
        if (vd->ann().contains(constants().ann.mzn_check_var)) {
          process_var = true;
        }
      } else {
        if (_outputObjective && vd->id()->idn() == -1 && vd->id()->v() == "_objective") {
          process_var = true;
        } else {
          if (vd->ann().contains(constants().ann.add_to_output)) {
            if (!_hadAddToOutput) {
              _outputVars.clear();
            }
            _hadAddToOutput = true;
            process_var = true;
          } else {
            if (!_hadAddToOutput) {
              process_var = false;
              if (vd->type().isvar()) {
                if (vd->e() != nullptr) {
                  if (auto* al = vd->e()->dynamicCast<ArrayLit>()) {
                    for (unsigned int i = 0; i < al->size(); i++) {
                      if ((*al)[i]->isa<AnonVar>()) {
                        process_var = true;
                        break;
                      }
                    }
                  } else if (vd->ann().contains(constants().ann.rhs_from_assignment)) {
                    process_var = true;
                  }
                } else {
                  process_var = true;
                }
              }
            }
          }
        }
      }
      if (process_var) {
        std::ostringstream s;
        s << vd->id()->str() << " = ";
        bool needArrayXd = false;
        if (vd->type().dim() > 0) {
          ArrayLit* al = nullptr;
          if ((vd->flat() != nullptr) && (vd->flat()->e() != nullptr)) {
            al = eval_array_lit(_e, vd->flat()->e());
          } else if (vd->e() != nullptr) {
            al = eval_array_lit(_e, vd->e());
          }
          if (al->size() > 0) {
            needArrayXd = true;
            s << "array" << vd->type().dim() << "d(";
            for (int i = 0; i < vd->type().dim(); i++) {
              unsigned int enumId =
                  (vd->type().enumId() != 0 ? _e.getArrayEnum(vd->type().enumId())[i] : 0);
              if (enumId != 0) {
                s << _e.getEnum(enumId)->e()->id()->str() << ", ";
              } else if (al != nullptr) {
                s << al->min(i) << ".." << al->max(i) << ", ";
              } else {
                IntSetVal* idxset = eval_intset(_e, vd->ti()->ranges()[i]->domain());
                s << *idxset << ", ";
              }
            }
          }
        }
        auto* sl = new StringLit(Location().introduce(), s.str());
        _outputVars.push_back(sl);

        std::vector<Expression*> showArgs(1);
        showArgs[0] = vd->id();
        Call* show = new Call(Location().introduce(), ASTString("showDzn"), showArgs);
        show->type(Type::parstring());
        FunctionI* fi = _e.model->matchFn(_e, show, false);
        assert(fi);
        show->decl(fi);
        _outputVars.push_back(show);
        std::string ends = needArrayXd ? ")" : "";
        ends += ";\n";
        auto* eol = new StringLit(Location().introduce(), ends);
        _outputVars.push_back(eol);
      }
    }
    void vOutputI(OutputI* oi) {
      if (_includeOutputItem) {
        _outputVars.push_back(new StringLit(Location().introduce(), "_output = "));
        Call* concat = new Call(Location().introduce(), ASTString("concat"), {oi->e()});
        concat->type(Type::parstring());
        FunctionI* fi = _e.model->matchFn(_e, concat, false);
        assert(fi);
        concat->decl(fi);
        Call* show = new Call(Location().introduce(), ASTString("showDzn"), {concat});
        show->type(Type::parstring());
        fi = _e.model->matchFn(_e, show, false);
        assert(fi);
        show->decl(fi);
        _outputVars.push_back(show);
        _outputVars.push_back(new StringLit(Location().introduce(), ";\n"));
      }

      oi->remove();
    }
  } dznov(e, outputObjective, includeOutputItem, outputForChecker, outputVars);

  iter_items(dznov, e.model);

  if (hasChecker && !outputForChecker) {
    outputVars.push_back(new StringLit(Location().introduce(), "_checker = "));
    auto* checker_output = new Call(Location().introduce(), ASTString("showCheckerOutput"), {});
    checker_output->type(Type::parstring());
    FunctionI* fi = e.model->matchFn(e, checker_output, false);
    assert(fi);
    checker_output->decl(fi);
    auto* show = new Call(Location().introduce(), ASTString("showDzn"), {checker_output});
    show->type(Type::parstring());
    fi = e.model->matchFn(e, show, false);
    assert(fi);
    show->decl(fi);
    outputVars.push_back(show);
    outputVars.push_back(new StringLit(Location().introduce(), ";\n"));
  }

  auto* newOutputItem =
      new OutputI(Location().introduce(), new ArrayLit(Location().introduce(), outputVars));
  e.model->addItem(newOutputItem);
}

ArrayLit* create__json_output(EnvI& e, bool outputObjective, bool includeOutputItem,
                              bool hasChecker) {
  std::vector<Expression*> outputVars;
  outputVars.push_back(new StringLit(Location().introduce(), "{\n"));

  class JSONOVisitor : public ItemVisitor {
  protected:
    EnvI& _e;
    bool _outputObjective;
    bool _includeOutputItem;
    std::vector<Expression*>& _outputVars;
    bool _hadAddToOutput;

  public:
    bool firstVar;
    JSONOVisitor(EnvI& e, bool outputObjective, bool includeOutputItem,
                 std::vector<Expression*>& outputVars)
        : _e(e),
          _outputObjective(outputObjective),
          _outputVars(outputVars),
          _includeOutputItem(includeOutputItem),
          _hadAddToOutput(false),
          firstVar(true) {}
    void vVarDeclI(VarDeclI* vdi) {
      VarDecl* vd = vdi->e();
      bool process_var = false;
      if (_outputObjective && vd->id()->idn() == -1 && vd->id()->v() == "_objective") {
        process_var = true;
      } else {
        if (vd->ann().contains(constants().ann.add_to_output)) {
          if (!_hadAddToOutput) {
            _outputVars.clear();
            _outputVars.push_back(new StringLit(Location().introduce(), "{\n"));
            firstVar = true;
          }
          _hadAddToOutput = true;
          process_var = true;
        } else {
          if (!_hadAddToOutput) {
            process_var =
                vd->type().isvar() &&
                (vd->e() == nullptr || vd->ann().contains(constants().ann.rhs_from_assignment));
          }
        }
      }
      if (process_var) {
        std::ostringstream s;
        if (firstVar) {
          firstVar = false;
        } else {
          s << ",\n";
        }
        s << "  \"" << vd->id()->str() << "\""
          << " : ";
        auto* sl = new StringLit(Location().introduce(), s.str());
        _outputVars.push_back(sl);

        std::vector<Expression*> showArgs(1);
        showArgs[0] = vd->id();
        Call* show = new Call(Location().introduce(), "showJSON", showArgs);
        show->type(Type::parstring());
        FunctionI* fi = _e.model->matchFn(_e, show, false);
        assert(fi);
        show->decl(fi);
        _outputVars.push_back(show);
      }
    }
    void vOutputI(OutputI* oi) {
      if (_includeOutputItem) {
        std::ostringstream s;
        if (firstVar) {
          firstVar = false;
        } else {
          s << ",\n";
        }
        s << "  \"_output\""
          << " : ";
        auto* sl = new StringLit(Location().introduce(), s.str());
        _outputVars.push_back(sl);
        Call* concat = new Call(Location().introduce(), ASTString("concat"), {oi->e()});
        concat->type(Type::parstring());
        FunctionI* fi = _e.model->matchFn(_e, concat, false);
        assert(fi);
        concat->decl(fi);
        Call* show = new Call(Location().introduce(), ASTString("showJSON"), {concat});
        show->type(Type::parstring());
        fi = _e.model->matchFn(_e, show, false);
        assert(fi);
        show->decl(fi);
        _outputVars.push_back(show);
      }

      oi->remove();
    }
  } jsonov(e, outputObjective, includeOutputItem, outputVars);

  iter_items(jsonov, e.model);

  if (hasChecker) {
    std::ostringstream s;
    if (jsonov.firstVar) {
      jsonov.firstVar = false;
    } else {
      s << ",\n";
    }
    s << "  \"_checker\""
      << " : ";
    auto* sl = new StringLit(Location().introduce(), s.str());
    outputVars.push_back(sl);
    Call* checker_output = new Call(Location().introduce(), ASTString("showCheckerOutput"), {});
    checker_output->type(Type::parstring());
    FunctionI* fi = e.model->matchFn(e, checker_output, false);
    assert(fi);
    checker_output->decl(fi);
    Call* show = new Call(Location().introduce(), ASTString("showJSON"), {checker_output});
    show->type(Type::parstring());
    fi = e.model->matchFn(e, show, false);
    assert(fi);
    show->decl(fi);
    outputVars.push_back(show);
  }

  outputVars.push_back(new StringLit(Location().introduce(), "\n}\n"));
  return new ArrayLit(Location().introduce(), outputVars);
}
void create_json_output_item(EnvI& e, bool outputObjective, bool includeOutputItem,
                             bool hasChecker) {
  auto* newOutputItem =
      new OutputI(Location().introduce(),
                  create__json_output(e, outputObjective, includeOutputItem, hasChecker));
  e.model->addItem(newOutputItem);
}

void create_output(EnvI& e, FlatteningOptions::OutputMode outputMode, bool outputObjective,
                   bool includeOutputItem, bool hasChecker) {
  // Create new output model
  OutputI* outputItem = nullptr;
  GCLock lock;

  switch (outputMode) {
    case FlatteningOptions::OUTPUT_DZN:
      create_dzn_output_item(e, outputObjective, includeOutputItem, hasChecker, false);
      break;
    case FlatteningOptions::OUTPUT_JSON:
      create_json_output_item(e, outputObjective, includeOutputItem, hasChecker);
      break;
    case FlatteningOptions::OUTPUT_CHECKER:
      create_dzn_output_item(e, outputObjective, includeOutputItem, hasChecker, true);
      break;
    default:
      if (e.model->outputItem() == nullptr) {
        create_dzn_output_item(e, outputObjective, false, false, false);
      }
      break;
  }

  // Copy output item from model into output model
  outputItem = copy(e, e.cmap, e.model->outputItem())->cast<OutputI>();
  make_par(e, outputItem->e());
  e.output->addItem(outputItem);

  // Copy all function definitions that are required for output into the output model
  class CollectFunctions : public EVisitor {
  public:
    EnvI& env;
    CollectFunctions(EnvI& env0) : env(env0) {}
    static bool enter(Expression* e) {
      if (e->type().isvar()) {
        Type t = e->type();
        t.ti(Type::TI_PAR);
        e->type(t);
      }
      return true;
    }
    void vId(Id& i) {
      // Also collect functions from output_only variables we depend on
      if ((i.decl() != nullptr) && i.decl()->ann().contains(constants().ann.output_only)) {
        top_down(*this, i.decl()->e());
      }
    }
    void vCall(Call& c) {
      std::vector<Type> tv(c.argCount());
      for (unsigned int i = c.argCount(); (i--) != 0U;) {
        tv[i] = c.arg(i)->type();
        tv[i].ti(Type::TI_PAR);
      }
      FunctionI* decl = env.output->matchFn(env, c.id(), tv, false);
      FunctionI* origdecl = env.model->matchFn(env, c.id(), tv, false);
      bool canReuseDecl = (decl != nullptr);
      if (canReuseDecl && (origdecl != nullptr)) {
        // Check if this is the exact same overloaded declaration as in the model
        for (unsigned int i = 0; i < decl->params().size(); i++) {
          if (decl->params()[i]->type() != origdecl->params()[i]->type()) {
            // no, the types don't match, so we have to copy the original decl
            canReuseDecl = false;
            break;
          }
        }
      }
      Type t;
      if (!canReuseDecl) {
        if (origdecl == nullptr || !is_completely_par(env, origdecl, tv)) {
          std::ostringstream ss;
          ss << "function " << c.id() << " is used in output, par version needed";
          throw FlatteningError(env, c.loc(), ss.str());
        }
        if (!origdecl->fromStdLib()) {
          auto* decl_copy = copy(env, env.cmap, origdecl)->cast<FunctionI>();
          if (decl_copy != decl) {
            decl = decl_copy;
            (void)env.output->registerFn(env, decl, true);
            env.output->addItem(decl);
            if (decl->e() != nullptr) {
              make_par(env, decl->e());
              top_down(*this, decl->e());
            }
            CollectOccurrencesE ce(env.outputVarOccurrences, decl);
            top_down(ce, decl->e());
            top_down(ce, decl->ti());
            for (unsigned int i = decl->params().size(); (i--) != 0U;) {
              top_down(ce, decl->params()[i]);
            }
          }
        } else {
          decl = origdecl;
        }
      }
      c.decl(decl);
    }
  } _cf(e);
  top_down(_cf, outputItem->e());

  // If we are checking solutions using a checker model, all parameters of the checker model
  // have to be made available in the output model
  class OV1 : public ItemVisitor {
  public:
    EnvI& env;
    CollectFunctions& cf;
    OV1(EnvI& env0, CollectFunctions& cf0) : env(env0), cf(cf0) {}
    void vVarDeclI(VarDeclI* vdi) {
      if (vdi->e()->ann().contains(constants().ann.mzn_check_var)) {
        auto* output_vd = copy(env, env.cmap, vdi->e())->cast<VarDecl>();
        top_down(cf, output_vd);
      }
    }
  } _ov1(e, _cf);
  iter_items(_ov1, e.model);

  // Copying the output item and the functions it depends on has created copies
  // of all dependent VarDecls. However the output model does not contain VarDeclIs for
  // these VarDecls yet. This iterator processes all variable declarations of the
  // original model, and if they were copied (i.e., if the output model depends on them),
  // the corresponding VarDeclI is created in the output model.
  class OV2 : public ItemVisitor {
  public:
    EnvI& env;
    OV2(EnvI& env0) : env(env0) {}
    void vVarDeclI(VarDeclI* vdi) {
      auto idx = env.outputVarOccurrences.idx.find(vdi->e()->id());
      if (idx != env.outputVarOccurrences.idx.end()) {
        return;
      }
      if (Expression* vd_e = env.cmap.find(vdi->e())) {
        // We found a copied VarDecl, now need to create a VarDeclI
        auto* vd = vd_e->cast<VarDecl>();
        auto* vdi_copy = copy(env, env.cmap, vdi)->cast<VarDeclI>();

        Type t = vdi_copy->e()->ti()->type();
        t.ti(Type::TI_PAR);
        vdi_copy->e()->ti()->domain(nullptr);
        vdi_copy->e()->flat(vdi->e()->flat());
        bool isCheckVar = vdi_copy->e()->ann().contains(constants().ann.mzn_check_var);
        Call* checkVarEnum = vdi_copy->e()->ann().getCall(constants().ann.mzn_check_enum_var);
        vdi_copy->e()->ann().clear();
        if (isCheckVar) {
          vdi_copy->e()->ann().add(constants().ann.mzn_check_var);
        }
        if (checkVarEnum != nullptr) {
          vdi_copy->e()->ann().add(checkVarEnum);
        }
        vdi_copy->e()->introduced(false);
        IdMap<KeepAlive>::iterator it;
        if (!vdi->e()->type().isPar()) {
          if (vd->flat() == nullptr && vdi->e()->e() != nullptr && vdi->e()->e()->type().isPar()) {
            // Don't have a flat version of this variable, but the original has a right hand
            // side that is par, so we can use that.
            Expression* flate = eval_par(env, vdi->e()->e());
            output_vardecls(env, vdi_copy, flate);
            vd->e(flate);
          } else {
            vd = follow_id_to_decl(vd->id())->cast<VarDecl>();
            VarDecl* reallyFlat = vd->flat();
            while ((reallyFlat != nullptr) && reallyFlat != reallyFlat->flat()) {
              reallyFlat = reallyFlat->flat();
            }
            if (reallyFlat == nullptr) {
              // The variable doesn't have a flat version. This can only happen if
              // the original variable had type-inst var, but a right-hand-side that
              // was par, so follow_id_to_decl lead to a par variable.
              assert(vd->e() && vd->e()->type().isPar());
              Expression* flate = eval_par(env, vd->e());
              output_vardecls(env, vdi_copy, flate);
              vd->e(flate);
            } else if ((vd->flat()->e() != nullptr) && vd->flat()->e()->type().isPar()) {
              // We can use the right hand side of the flat version of this variable
              Expression* flate = copy(env, env.cmap, follow_id(reallyFlat->id()));
              output_vardecls(env, vdi_copy, flate);
              vd->e(flate);
            } else if ((it = env.reverseMappers.find(vd->id())) != env.reverseMappers.end()) {
              // Found a reverse mapper, so we need to add the mapping function to the
              // output model to map the FlatZinc value back to the model variable.
              Call* rhs = copy(env, env.cmap, it->second())->cast<Call>();
              check_output_par_fn(env, rhs);
              output_vardecls(env, vdi_copy, rhs);
              vd->e(rhs);
            } else if (cannot_use_rhs_for_output(env, vd->e())) {
              // If the VarDecl does not have a usable right hand side, it needs to be
              // marked as output in the FlatZinc
              vd->e(nullptr);
              assert(vd->flat());
              if (vd->type().dim() == 0) {
                vd->flat()->addAnnotation(constants().ann.output_var);
                check_rename_var(env, vd);
              } else {
                bool needOutputAnn = true;
                if ((reallyFlat->e() != nullptr) && reallyFlat->e()->isa<ArrayLit>()) {
                  auto* al = reallyFlat->e()->cast<ArrayLit>();
                  for (unsigned int i = 0; i < al->size(); i++) {
                    if (Id* id = (*al)[i]->dynamicCast<Id>()) {
                      if (env.reverseMappers.find(id) != env.reverseMappers.end()) {
                        needOutputAnn = false;
                        break;
                      }
                    }
                  }
                  if (!needOutputAnn) {
                    output_vardecls(env, vdi_copy, al);
                    vd->e(copy(env, env.cmap, al));
                  }
                }
                if (needOutputAnn) {
                  std::vector<Expression*> args(vdi->e()->type().dim());
                  for (unsigned int i = 0; i < args.size(); i++) {
                    if (vdi->e()->ti()->ranges()[i]->domain() == nullptr) {
                      args[i] =
                          new SetLit(Location().introduce(),
                                     eval_intset(env, vd->flat()->ti()->ranges()[i]->domain()));
                    } else {
                      args[i] = new SetLit(Location().introduce(),
                                           eval_intset(env, vd->ti()->ranges()[i]->domain()));
                    }
                  }
                  auto* al = new ArrayLit(Location().introduce(), args);
                  args.resize(1);
                  args[0] = al;
                  vd->flat()->addAnnotation(
                      new Call(Location().introduce(), constants().ann.output_array, args));
                  check_rename_var(env, vd);
                }
              }
            }
            if ((reallyFlat != nullptr) && env.outputFlatVarOccurrences.find(reallyFlat) == -1) {
              env.outputFlatVarOccurrences.addIndex(reallyFlat,
                                                    static_cast<int>(env.output->size()));
            }
          }
        } else {
          if (vd->flat() == nullptr && vdi->e()->e() != nullptr) {
            // Need to process right hand side of variable, since it may contain
            // identifiers that are only in the FlatZinc and that we would
            // therefore fail to copy into the output model
            output_vardecls(env, vdi_copy, vdi->e()->e());
          }
        }
        make_par(env, vdi_copy->e());
        env.outputVarOccurrences.addIndex(vdi_copy, static_cast<int>(env.output->size()));
        CollectOccurrencesE ce(env.outputVarOccurrences, vdi_copy);
        top_down(ce, vdi_copy->e());
        env.output->addItem(vdi_copy);
      }
    }
  } _ov2(e);
  iter_items(_ov2, e.model);

  CollectOccurrencesE ce(e.outputVarOccurrences, outputItem);
  top_down(ce, outputItem->e());

  e.model->mergeStdLib(e, e.output);
  process_deletions(e);
}

Expression* is_fixed_domain(EnvI& env, VarDecl* vd) {
  if (vd->type() != Type::varbool() && vd->type() != Type::varint() &&
      vd->type() != Type::varfloat()) {
    return nullptr;
  }
  Expression* e = vd->ti()->domain();
  if (e == constants().literalTrue || e == constants().literalFalse) {
    return e;
  }
  if (auto* sl = Expression::dynamicCast<SetLit>(e)) {
    if (sl->type().bt() == Type::BT_INT) {
      IntSetVal* isv = eval_intset(env, sl);
      return isv->min() == isv->max() ? IntLit::a(isv->min()) : nullptr;
    }
    if (sl->type().bt() == Type::BT_FLOAT) {
      FloatSetVal* fsv = eval_floatset(env, sl);
      return fsv->min() == fsv->max() ? FloatLit::a(fsv->min()) : nullptr;
    }
  }
  return nullptr;
}

void finalise_output(EnvI& e) {
  if (e.output->size() > 0) {
    // Adapt existing output model
    // (generated by repeated flattening)
    e.outputVarOccurrences.clear();
    for (unsigned int i = 0; i < e.output->size(); i++) {
      Item* item = (*e.output)[i];
      if (item->removed()) {
        continue;
      }
      switch (item->iid()) {
        case Item::II_VD: {
          VarDecl* vd = item->cast<VarDeclI>()->e();
          IdMap<KeepAlive>::iterator it;
          GCLock lock;
          VarDecl* reallyFlat = vd->flat();
          while ((reallyFlat != nullptr) && reallyFlat != reallyFlat->flat()) {
            reallyFlat = reallyFlat->flat();
          }
          if (vd->e() == nullptr) {
            if (((vd->flat()->e() != nullptr) && vd->flat()->e()->type().isPar()) ||
                (is_fixed_domain(e, vd->flat()) != nullptr)) {
              VarDecl* reallyFlat = vd->flat();
              while (reallyFlat != reallyFlat->flat()) {
                reallyFlat = reallyFlat->flat();
              }
              remove_is_output(reallyFlat);
              Expression* flate;
              if (Expression* fd = is_fixed_domain(e, vd->flat())) {
                flate = fd;
              } else {
                flate = copy(e, e.cmap, follow_id(reallyFlat->id()));
              }
              output_vardecls(e, item, flate);
              vd->e(flate);
            } else if ((it = e.reverseMappers.find(vd->id())) != e.reverseMappers.end()) {
              Call* rhs = copy(e, e.cmap, it->second())->cast<Call>();
              check_output_par_fn(e, rhs);
              remove_is_output(reallyFlat);

              output_vardecls(e, item, it->second()->cast<Call>());
              vd->e(rhs);

              if (e.varOccurrences.occurrences(reallyFlat) == 0 && reallyFlat->e() == nullptr) {
                auto it = e.varOccurrences.idx.find(reallyFlat->id());
                assert(it != e.varOccurrences.idx.end());
                e.flatRemoveItem((*e.flat())[it->second]->cast<VarDeclI>());
              }
            } else {
              // If the VarDecl does not have a usable right hand side, it needs to be
              // marked as output in the FlatZinc
              assert(vd->flat());

              bool needOutputAnn = true;
              if ((reallyFlat->e() != nullptr) && reallyFlat->e()->isa<Id>()) {
                Id* ident = reallyFlat->e()->cast<Id>();
                if (e.reverseMappers.find(ident) != e.reverseMappers.end()) {
                  needOutputAnn = false;
                  remove_is_output(vd);
                  remove_is_output(reallyFlat);

                  vd->e(copy(e, e.cmap, ident));
                  Type al_t(vd->e()->type());
                  al_t.ti(Type::TI_PAR);
                  vd->e()->type(al_t);

                  output_vardecls(e, item, ident);

                  if (e.varOccurrences.occurrences(reallyFlat) == 0) {
                    auto it = e.varOccurrences.idx.find(reallyFlat->id());
                    assert(it != e.varOccurrences.idx.end());
                    e.flatRemoveItem((*e.flat())[it->second]->cast<VarDeclI>());
                  }
                }
              } else if ((reallyFlat->e() != nullptr) && reallyFlat->e()->isa<ArrayLit>()) {
                auto* al = reallyFlat->e()->cast<ArrayLit>();
                for (unsigned int i = 0; i < al->size(); i++) {
                  if (Id* ident = follow_id_to_value((*al)[i])->dynamicCast<Id>()) {
                    if (e.reverseMappers.find(ident) != e.reverseMappers.end()) {
                      needOutputAnn = false;
                      break;
                    }
                  }
                }
                if (!needOutputAnn) {
                  remove_is_output(vd);
                  remove_is_output(reallyFlat);
                  if (e.varOccurrences.occurrences(reallyFlat) == 0) {
                    auto it = e.varOccurrences.idx.find(reallyFlat->id());
                    assert(it != e.varOccurrences.idx.end());
                    e.flatRemoveItem((*e.flat())[it->second]->cast<VarDeclI>());
                  }

                  output_vardecls(e, item, al);
                  vd->e(copy(e, e.cmap, al));
                  Type al_t(vd->e()->type());
                  al_t.ti(Type::TI_PAR);
                  vd->e()->type(al_t);
                }
              }
              if (needOutputAnn) {
                if (!is_output(vd->flat())) {
                  GCLock lock;
                  if (vd->type().dim() == 0) {
                    vd->flat()->addAnnotation(constants().ann.output_var);
                  } else {
                    std::vector<Expression*> args(vd->type().dim());
                    for (unsigned int i = 0; i < args.size(); i++) {
                      if (vd->ti()->ranges()[i]->domain() == nullptr) {
                        args[i] =
                            new SetLit(Location().introduce(),
                                       eval_intset(e, vd->flat()->ti()->ranges()[i]->domain()));
                      } else {
                        args[i] = new SetLit(Location().introduce(),
                                             eval_intset(e, vd->ti()->ranges()[i]->domain()));
                      }
                    }
                    auto* al = new ArrayLit(Location().introduce(), args);
                    args.resize(1);
                    args[0] = al;
                    vd->flat()->addAnnotation(
                        new Call(Location().introduce(), constants().ann.output_array, args));
                  }
                  check_rename_var(e, vd);
                }
              }
            }
            vd->flat(nullptr);
            // Remove enum type
            Type vdt = vd->type();
            vdt.enumId(0);
            vd->type(vdt);
            vd->ti()->type(vdt);
          }
          e.outputVarOccurrences.addIndex(item->cast<VarDeclI>(), static_cast<int>(i));
          CollectOccurrencesE ce(e.outputVarOccurrences, item);
          top_down(ce, vd);
        } break;
        case Item::II_OUT: {
          CollectOccurrencesE ce(e.outputVarOccurrences, item);
          top_down(ce, item->cast<OutputI>()->e());
        } break;
        case Item::II_FUN: {
          CollectOccurrencesE ce(e.outputVarOccurrences, item);
          top_down(ce, item->cast<FunctionI>()->e());
          top_down(ce, item->cast<FunctionI>()->ti());
          for (unsigned int i = item->cast<FunctionI>()->params().size(); (i--) != 0U;) {
            top_down(ce, item->cast<FunctionI>()->params()[i]);
          }
        } break;
        default:
          throw FlatteningError(e, item->loc(), "invalid item in output model");
      }
    }
  }
  process_deletions(e);
}
}  // namespace MiniZinc
