#pragma once

#include <map>
#include <optional>
#include <variant>

#include <absl/container/flat_hash_map.h>

#include "libexpr/symbol-table.hh"
#include "libexpr/value.hh"
#include "libutil/types.hh"  // TODO(tazjin): audit this include

namespace nix {

MakeError(EvalError, Error);
MakeError(ParseError, Error);
MakeError(AssertionError, EvalError);
MakeError(ThrownError, AssertionError);
MakeError(Abort, EvalError);
MakeError(TypeError, EvalError);
MakeError(UndefinedVarError, Error);
MakeError(RestrictedPathError, Error);

/* Position objects. */

struct Pos {
  std::optional<Symbol> file;
  unsigned int line, column;
  Pos(const std::optional<Symbol>& file, unsigned int line, unsigned int column)
      : file(file), line(line), column(column){};

  // TODO(tazjin): remove this - empty pos is never useful
  Pos() : file(std::nullopt), line(0), column(0){};

  operator bool() const { return line != 0; }

  bool operator<(const Pos& p2) const {
    if (!file.has_value()) {
      return true;
    }

    if (!line) {
      return p2.line;
    }
    if (!p2.line) {
      return false;
    }
    int d = ((std::string)file.value()).compare((std::string)p2.file.value());
    if (d < 0) {
      return true;
    }
    if (d > 0) {
      return false;
    }
    if (line < p2.line) {
      return true;
    }
    if (line > p2.line) {
      return false;
    }
    return column < p2.column;
  }
};

extern Pos noPos;

std::ostream& operator<<(std::ostream& str, const Pos& pos);

struct Env;
struct Value;
class EvalState;
struct StaticEnv;

/* An attribute path is a sequence of attribute names. */
using AttrName = std::variant<Symbol, Expr*>;

typedef std::vector<AttrName> AttrPath;

std::string showAttrPath(const AttrPath& attrPath);

/* Abstract syntax of Nix expressions. */

struct Expr {
  virtual ~Expr(){};
  virtual void show(std::ostream& str) const;
  virtual void bindVars(const StaticEnv& env);
  virtual void eval(EvalState& state, Env& env, Value& v);
  virtual Value* maybeThunk(EvalState& state, Env& env);
};

std::ostream& operator<<(std::ostream& str, const Expr& e);

#define COMMON_METHODS                             \
  void show(std::ostream& str) const;              \
  void eval(EvalState& state, Env& env, Value& v); \
  void bindVars(const StaticEnv& env);

struct ExprInt : Expr {
  NixInt n;
  Value v;
  ExprInt(NixInt n) : n(n) { mkInt(v, n); };
  COMMON_METHODS
  Value* maybeThunk(EvalState& state, Env& env);
};

struct ExprFloat : Expr {
  NixFloat nf;
  Value v;
  ExprFloat(NixFloat nf) : nf(nf) { mkFloat(v, nf); };
  COMMON_METHODS
  Value* maybeThunk(EvalState& state, Env& env);
};

struct ExprString : Expr {
  Symbol s;
  Value v;
  ExprString(const Symbol& s) : s(s) { mkString(v, s); };
  COMMON_METHODS
  Value* maybeThunk(EvalState& state, Env& env);
};

/* Temporary class used during parsing of indented strings. */
struct ExprIndStr : Expr {
  std::string s;
  ExprIndStr(const std::string& s) : s(s){};
};

struct ExprPath : Expr {
  std::string s;
  Value v;
  ExprPath(const std::string& s) : s(s) { mkPathNoCopy(v, this->s.c_str()); };
  COMMON_METHODS
  Value* maybeThunk(EvalState& state, Env& env);
};

struct ExprVar : Expr {
  Pos pos;
  Symbol name;

  /* Whether the variable comes from an environment (e.g. a rec, let
     or function argument) or from a "with". */
  bool fromWith;

  /* In the former case, the value is obtained by going `level'
     levels up from the current environment and getting the
     `displ'th value in that environment.  In the latter case, the
     value is obtained by getting the attribute named `name' from
     the set stored in the environment that is `level' levels up
     from the current one.*/
  unsigned int level;
  unsigned int displ;

  ExprVar(const Symbol& name) : name(name){};
  ExprVar(const Pos& pos, const Symbol& name) : pos(pos), name(name){};
  COMMON_METHODS
  Value* maybeThunk(EvalState& state, Env& env);
};

// [tazjin] I *think* that this struct describes the syntactic
// construct for "selecting" something out of an attribute set, e.g.
// `a.b.c` => ExprSelect{"b", "c"}.
//
// Each path element has got a pointer to an expression, which seems
// to be the thing preceding its period, but afaict that is only set
// for the first one in a path.
struct ExprSelect : Expr {
  Pos pos;
  Expr *e, *def;
  AttrPath attrPath;
  ExprSelect(const Pos& pos, Expr* e, const AttrPath& attrPath, Expr* def)
      : pos(pos), e(e), def(def), attrPath(attrPath){};
  ExprSelect(const Pos& pos, Expr* e, const Symbol& name)
      : pos(pos), e(e), def(0) {
    attrPath.push_back(AttrName(name));
  };
  COMMON_METHODS
};

struct ExprOpHasAttr : Expr {
  Expr* e;
  AttrPath attrPath;
  ExprOpHasAttr(Expr* e, const AttrPath& attrPath) : e(e), attrPath(attrPath){};
  COMMON_METHODS
};

struct ExprAttrs : Expr {
  bool recursive;

  struct AttrDef {
    bool inherited;
    Expr* e;
    Pos pos;
    unsigned int displ;  // displacement
    AttrDef(Expr* e, const Pos& pos, bool inherited = false)
        : inherited(inherited), e(e), pos(pos), displ(0){};
    AttrDef(){};
  };

  typedef absl::flat_hash_map<Symbol, AttrDef> AttrDefs;
  AttrDefs attrs;

  struct DynamicAttrDef {
    Expr *nameExpr, *valueExpr;
    Pos pos;
    DynamicAttrDef(Expr* nameExpr, Expr* valueExpr, const Pos& pos)
        : nameExpr(nameExpr), valueExpr(valueExpr), pos(pos){};
  };

  typedef std::vector<DynamicAttrDef> DynamicAttrDefs;
  DynamicAttrDefs dynamicAttrs;

  ExprAttrs() : recursive(false){};
  COMMON_METHODS
};

struct ExprList : Expr {
  std::vector<Expr*> elems;
  ExprList(){};
  COMMON_METHODS
};

struct Formal {
  Symbol name;
  Expr* def;  // def = default, not definition
  Formal(const Symbol& name, Expr* def) : name(name), def(def){};
};

// Describes structured function arguments (e.g. `{ a }: ...`)
struct Formals {
  typedef std::list<Formal> Formals_;
  Formals_ formals;
  std::set<Symbol> argNames;  // used during parsing
  bool ellipsis;
};

struct ExprLambda : Expr {
 public:
  Pos pos;
  std::optional<Symbol> name;
  Symbol arg;
  bool matchAttrs;
  Formals* formals;
  Expr* body;
  ExprLambda(const Pos& pos, const Symbol& arg, bool matchAttrs,
             Formals* formals, Expr* body)
      : pos(pos),
        arg(arg),
        matchAttrs(matchAttrs),
        formals(formals),
        body(body) {
    if (!arg.empty() && formals &&
        formals->argNames.find(arg) != formals->argNames.end()) {
      throw ParseError(
          format("duplicate formal function argument '%1%' at %2%") % arg %
          pos);
    }
  };
  void setName(Symbol& name);
  std::string showNamePos() const;
  COMMON_METHODS
};

struct ExprLet : Expr {
  ExprAttrs* attrs;
  Expr* body;
  ExprLet(ExprAttrs* attrs, Expr* body) : attrs(attrs), body(body){};
  COMMON_METHODS
};

struct ExprWith : Expr {
  Pos pos;
  Expr *attrs, *body;
  size_t prevWith;
  ExprWith(const Pos& pos, Expr* attrs, Expr* body)
      : pos(pos), attrs(attrs), body(body){};
  COMMON_METHODS
};

struct ExprIf : Expr {
  Expr *cond, *then, *else_;
  ExprIf(Expr* cond, Expr* then, Expr* else_)
      : cond(cond), then(then), else_(else_){};
  COMMON_METHODS
};

struct ExprAssert : Expr {
  Pos pos;
  Expr *cond, *body;
  ExprAssert(const Pos& pos, Expr* cond, Expr* body)
      : pos(pos), cond(cond), body(body){};
  COMMON_METHODS
};

struct ExprOpNot : Expr {
  Expr* e;
  ExprOpNot(Expr* e) : e(e){};
  COMMON_METHODS
};

#define MakeBinOp(name, s)                                                 \
  struct name : Expr {                                                     \
    Pos pos;                                                               \
    Expr *e1, *e2;                                                         \
    name(Expr* e1, Expr* e2) : e1(e1), e2(e2){};                           \
    name(const Pos& pos, Expr* e1, Expr* e2) : pos(pos), e1(e1), e2(e2){}; \
    void show(std::ostream& str) const {                                   \
      str << "(" << *e1 << " " s " " << *e2 << ")";                        \
    }                                                                      \
    void bindVars(const StaticEnv& env) {                                  \
      e1->bindVars(env);                                                   \
      e2->bindVars(env);                                                   \
    }                                                                      \
    void eval(EvalState& state, Env& env, Value& v);                       \
  };

MakeBinOp(ExprApp, "");
MakeBinOp(ExprOpEq, "==");
MakeBinOp(ExprOpNEq, "!=");
MakeBinOp(ExprOpAnd, "&&");
MakeBinOp(ExprOpOr, "||");
MakeBinOp(ExprOpImpl, "->");
MakeBinOp(ExprOpUpdate, "//");
MakeBinOp(ExprOpConcatLists, "++");

struct ExprConcatStrings : Expr {
  Pos pos;
  bool forceString;
  std::vector<Expr*>* es;
  ExprConcatStrings(const Pos& pos, bool forceString, std::vector<Expr*>* es)
      : pos(pos), forceString(forceString), es(es){};
  COMMON_METHODS
};

struct ExprPos : Expr {
  Pos pos;
  ExprPos(const Pos& pos) : pos(pos){};
  COMMON_METHODS
};

/* Static environments are used to map variable names onto (level,
   displacement) pairs used to obtain the value of the variable at
   runtime. */
struct StaticEnv {
  bool isWith;
  const StaticEnv* up;
  typedef absl::flat_hash_map<Symbol, unsigned int> Vars;
  Vars vars;
  StaticEnv(bool isWith, const StaticEnv* up) : isWith(isWith), up(up){};
};

}  // namespace nix