/// Library to convert AST nodes back to text.
///
/// See the [NodeToText] extension for details.
library utils.node_to_text;

import 'package:charcode/charcode.dart';
import 'package:sqlparser/sqlparser.dart';
import 'package:sqlparser/src/reader/tokenizer/token.dart';

/// Defines the [toSql] extension method that turns ast nodes into a compatible
/// textual representation.
///
/// Parsing the output of [toSql] will result in an equal AST.
extension NodeToText on AstNode {
  /// Obtains a textual representation for AST nodes.
  ///
  /// Parsing the output of [toSql] will result in an equal AST. Since only the
  /// AST is used, the output will not contain comments. It's possible for the
  /// output to have more than just whitespace changes if there are multiple
  /// ways to represent an equivalent node (e.g. the no-op `FOR EACH ROW` on
  /// triggers).
  String toSql() {
    final builder = _NodeSqlBuilder();
    builder.visit(this, null);
    return builder._buffer.toString();
  }
}

class _NodeSqlBuilder extends AstVisitor<void, void> {
  final StringBuffer _buffer = StringBuffer();
  bool _needsSpace = false;

  void _join(Iterable<AstNode> nodes, String separatingSymbol) {
    var isFirst = true;

    for (final node in nodes) {
      if (!isFirst) {
        _symbol(separatingSymbol, spaceAfter: true);
      }

      visit(node, null);
      isFirst = false;
    }
  }

  void _identifier(String identifier,
      {bool spaceBefore = true, bool spaceAfter = true}) {
    if (isKeywordLexeme(identifier) || identifier.contains(' ')) {
      identifier = '"$identifier"';
    }

    _symbol(identifier, spaceBefore: spaceBefore, spaceAfter: spaceAfter);
  }

  void _ifNotExists(bool ifNotExists) {
    if (ifNotExists) {
      _keyword(TokenType.$if);
      _keyword(TokenType.not);
      _keyword(TokenType.exists);
    }
  }

  void _keyword(TokenType type) {
    _symbol(reverseKeywords[type], spaceAfter: true, spaceBefore: true);
  }

  void _space() => _buffer.writeCharCode($space);

  void _stringLiteral(String content) {
    _symbol("'$content'", spaceBefore: true, spaceAfter: true);
  }

  void _symbol(String lexeme,
      {bool spaceBefore = false, bool spaceAfter = false}) {
    if (_needsSpace && spaceBefore) {
      _space();
    }

    _buffer.write(lexeme);
    _needsSpace = spaceAfter;
  }

  void _where(Expression /*?*/ where) {
    if (where != null) {
      _keyword(TokenType.where);
      visit(where, null);
    }
  }

  @override
  void visitAggregateExpression(AggregateExpression e, void arg) {
    _symbol(e.name);

    _symbol('(');
    visit(e.parameters, arg);
    _symbol(')');

    if (e.filter != null) {
      _keyword(TokenType.filter);
      _symbol('(', spaceBefore: true);
      _keyword(TokenType.where);
      visit(e.filter, arg);
      _symbol(')', spaceAfter: true);
    }

    if (e.windowDefinition != null) {
      _keyword(TokenType.over);
      visit(e.windowDefinition, arg);
    } else if (e.windowName != null) {
      _keyword(TokenType.over);
      _identifier(e.windowName);
    }
  }

  @override
  void visitBetweenExpression(BetweenExpression e, void arg) {
    visit(e.check, arg);

    if (e.not) {
      _keyword(TokenType.not);
    }

    _keyword(TokenType.between);
    visit(e.lower, arg);
    _keyword(TokenType.and);
    visit(e.upper, arg);
  }

  @override
  void visitBinaryExpression(BinaryExpression e, void arg) {
    visit(e.left, arg);

    final symbol = const {
      TokenType.doublePipe: '||',
      TokenType.star: '*',
      TokenType.slash: '/',
      TokenType.percent: '%',
      TokenType.plus: '+',
      TokenType.minus: '-',
      TokenType.shiftLeft: '<<',
      TokenType.shiftRight: '>>',
      TokenType.ampersand: '&',
      TokenType.pipe: '|',
      TokenType.less: '<',
      TokenType.lessEqual: '<=',
      TokenType.more: '>',
      TokenType.moreEqual: '>=',
      TokenType.equal: '=',
      TokenType.doubleEqual: '==',
      TokenType.exclamationEqual: '!=',
      TokenType.lessMore: '<>',
    }[e.operator.type];

    if (symbol != null) {
      _symbol(symbol, spaceBefore: true, spaceAfter: true);
    } else {
      _keyword(e.operator.type);
    }

    visit(e.right, arg);
  }

  @override
  void visitBlock(Block block, void arg) {
    _keyword(TokenType.begin);
    for (final stmt in block.statements) {
      visit(stmt, arg);
      _symbol(';');
    }
    _keyword(TokenType.end);
  }

  @override
  void visitBooleanLiteral(BooleanLiteral e, void arg) {
    _keyword(e.value ? TokenType.$true : TokenType.$false);
  }

  @override
  void visitCaseExpression(CaseExpression e, void arg) {
    _keyword(TokenType.$case);
    visitNullable(e.base, arg);
    visitList(e.whens, arg);

    final elseExpr = e.elseExpr;
    if (elseExpr != null) {
      _keyword(TokenType.$else);
      visit(elseExpr, arg);
    }

    _keyword(TokenType.end);
  }

  @override
  void visitCastExpression(CastExpression e, void arg) {
    _keyword(TokenType.cast);
    _symbol('(');
    visit(e.operand, arg);
    _keyword(TokenType.as);
    _symbol(e.typeName, spaceBefore: true);
    _symbol(')', spaceAfter: true);
  }

  @override
  void visitCollateExpression(CollateExpression e, void arg) {
    visit(e.inner, arg);
    _keyword(TokenType.collate);
    _identifier(e.collation);
  }

  void _conflictClause(ConflictClause clause) {
    if (clause != null) {
      _keyword(TokenType.on);
      _keyword(TokenType.conflict);

      _keyword(const {
        ConflictClause.rollback: TokenType.rollback,
        ConflictClause.abort: TokenType.abort,
        ConflictClause.fail: TokenType.fail,
        ConflictClause.ignore: TokenType.ignore,
        ConflictClause.replace: TokenType.replace,
      }[clause]);
    }
  }

  @override
  void visitColumnConstraint(ColumnConstraint e, void arg) {
    if (e.name != null) {
      _keyword(TokenType.constraint);
      _identifier(e.name);
    }

    e.when(
      primaryKey: (primaryKey) {
        _keyword(TokenType.primary);
        _keyword(TokenType.key);
        _orderingMode(primaryKey.mode);
        _conflictClause(primaryKey.onConflict);
        if (primaryKey.autoIncrement) _keyword(TokenType.autoincrement);
      },
      notNull: (notNull) {
        _keyword(TokenType.not);
        _keyword(TokenType.$null);
        _conflictClause(notNull.onConflict);
      },
      unique: (unique) {
        _keyword(TokenType.unique);
        _conflictClause(unique.onConflict);
      },
      check: (check) {
        _keyword(TokenType.check);
        _symbol('(', spaceBefore: true);
        visit(check.expression, arg);
        _symbol(')', spaceAfter: true);
      },
      isDefault: (def) {
        _keyword(TokenType.$default);
        final expr = def.expression;
        if (expr is Literal) {
          visit(expr, arg);
        } else {
          _symbol('(', spaceBefore: true);
          visit(expr, arg);
          _symbol(')', spaceAfter: true);
        }
      },
      collate: (collate) {
        _keyword(TokenType.collate);
        _identifier(collate.collation);
      },
      foreignKey: (foreignKey) {
        visit(foreignKey.clause, arg);
      },
    );
  }

  @override
  void visitColumnDefinition(ColumnDefinition e, void arg) {
    _identifier(e.columnName);
    if (e.typeName != null) {
      _symbol(e.typeName, spaceAfter: true, spaceBefore: true);
    }

    visitList(e.constraints, arg);
  }

  @override
  void visitCommonTableExpression(CommonTableExpression e, void arg) {
    _identifier(e.cteTableName);
    if (e.columnNames != null) {
      _symbol('(${e.columnNames.join(', ')})');
    }

    _keyword(TokenType.as);
    _symbol('(', spaceBefore: true);
    visit(e.as, arg);
    _symbol(')', spaceAfter: true);
  }

  @override
  void visitCompoundSelectPart(CompoundSelectPart e, void arg) {
    switch (e.mode) {
      case CompoundSelectMode.union:
        _keyword(TokenType.union);
        break;
      case CompoundSelectMode.unionAll:
        _keyword(TokenType.union);
        _keyword(TokenType.all);
        break;
      case CompoundSelectMode.intersect:
        _keyword(TokenType.intersect);
        break;
      case CompoundSelectMode.except:
        _keyword(TokenType.except);
        break;
    }

    visit(e.select, arg);
  }

  @override
  void visitCompoundSelectStatement(CompoundSelectStatement e, void arg) {
    visitNullable(e.withClause, arg);
    visit(e.base, arg);
    visitList(e.additional, arg);
  }

  @override
  void visitCreateIndexStatement(CreateIndexStatement e, void arg) {
    _keyword(TokenType.create);
    if (e.unique) {
      _keyword(TokenType.unique);
    }
    _keyword(TokenType.$index);
    _ifNotExists(e.ifNotExists);

    _identifier(e.indexName);
    _keyword(TokenType.on);
    visit(e.on, arg);

    _symbol('(', spaceBefore: true);
    _join(e.columns, ',');
    _symbol(')', spaceAfter: true);

    _where(e.where);
  }

  @override
  void visitCreateTableStatement(CreateTableStatement e, void arg) {
    _keyword(TokenType.create);
    _keyword(TokenType.table);
    _ifNotExists(e.ifNotExists);

    _identifier(e.tableName);
    _symbol('(');
    _join([...e.columns, ...e.tableConstraints], ',');
    _symbol(')');

    if (e.withoutRowId) {
      _keyword(TokenType.without);
      _keyword(TokenType.rowid);
    }
  }

  @override
  void visitCreateTriggerStatement(CreateTriggerStatement e, void arg) {
    _keyword(TokenType.create);
    _keyword(TokenType.trigger);
    _ifNotExists(e.ifNotExists);

    _identifier(e.triggerName);

    switch (e.mode) {
      case TriggerMode.before:
        _keyword(TokenType.before);
        break;
      case TriggerMode.after:
        _keyword(TokenType.after);
        break;
      case TriggerMode.insteadOf:
        _keyword(TokenType.instead);
        _keyword(TokenType.of);
        break;
      default:
        // Can happen if e.mode == null
        break;
    }

    visit(e.target, arg);

    _keyword(TokenType.on);
    visit(e.onTable, arg);

    if (e.when != null) {
      _keyword(TokenType.when);
      visit(e.when, arg);
    }

    visit(e.action, arg);
  }

  @override
  void visitCreateViewStatement(CreateViewStatement e, void arg) {
    _keyword(TokenType.create);
    _keyword(TokenType.view);
    _ifNotExists(e.ifNotExists);

    _identifier(e.viewName);

    if (e.columns != null) {
      _symbol('(', spaceBefore: true);
      _symbol(e.columns.join(','));
      _symbol(')', spaceAfter: true);
    }

    _keyword(TokenType.as);
    visit(e.query, arg);
  }

  @override
  void visitCreateVirtualTableStatement(
      CreateVirtualTableStatement e, void arg) {
    _keyword(TokenType.create);
    _keyword(TokenType.virtual);
    _keyword(TokenType.table);
    _ifNotExists(e.ifNotExists);

    _identifier(e.tableName);
    _keyword(TokenType.using);
    _identifier(e.moduleName);

    _symbol('(${e.argumentContent.join(', ')})');
  }

  @override
  void visitDartPlaceholder(DartPlaceholder e, void arg) {
    _symbol(r'$', spaceBefore: true);
    _symbol(e.name, spaceAfter: true);
  }

  @override
  void visitDefaultValues(DefaultValues e, void arg) {
    _keyword(TokenType.$default);
    _keyword(TokenType.$values);
  }

  @override
  void visitDeferrableClause(DeferrableClause e, void arg) {
    if (e.not) {
      _keyword(TokenType.not);
    }
    _keyword(TokenType.deferrable);

    switch (e.declaredInitially) {
      case InitialDeferrableMode.deferred:
        _keyword(TokenType.initially);
        _keyword(TokenType.deferred);
        break;
      case InitialDeferrableMode.immediate:
        _keyword(TokenType.initially);
        _keyword(TokenType.immediate);
        break;
      default:
        // declaredInitially == null, don't do anything
        break;
    }
  }

  @override
  void visitDeleteStatement(DeleteStatement e, void arg) {
    visitNullable(e.withClause, arg);

    _keyword(TokenType.delete);
    _keyword(TokenType.from);
    visit(e.from, null);
    _where(e.where);
  }

  @override
  void visitDeleteTriggerTarget(DeleteTarget e, void arg) {
    _keyword(TokenType.delete);
  }

  @override
  void visitDoNothing(DoNothing e, void arg) {
    _keyword(TokenType.nothing);
  }

  @override
  void visitDoUpdate(DoUpdate e, void arg) {
    _keyword(TokenType.update);
    _keyword(TokenType.set);
    _join(e.set, ',');
    _where(e.where);
  }

  @override
  void visitExists(ExistsExpression e, void arg) {
    _keyword(TokenType.exists);
    _symbol('(', spaceBefore: true);
    visit(e.select, null);
    _symbol(')', spaceAfter: true);
  }

  @override
  void visitExpressionFunctionParameters(ExprFunctionParameters e, void arg) {
    if (e.distinct) {
      _keyword(TokenType.distinct);
    }
    _join(e.parameters, ',');
  }

  @override
  void visitForeignKeyClause(ForeignKeyClause e, void arg) {
    _keyword(TokenType.references);
    visit(e.foreignTable, arg);

    if (e.columnNames.isNotEmpty) {
      _symbol('(');
      _join(e.columnNames, ',');
      _symbol(')');
    }

    void referenceAction(ReferenceAction action) {
      switch (action) {
        case ReferenceAction.setNull:
          _keyword(TokenType.set);
          _keyword(TokenType.$null);
          break;
        case ReferenceAction.setDefault:
          _keyword(TokenType.set);
          _keyword(TokenType.$default);
          break;
        case ReferenceAction.cascade:
          _keyword(TokenType.cascade);
          break;
        case ReferenceAction.restrict:
          _keyword(TokenType.restrict);
          break;
        case ReferenceAction.noAction:
          _keyword(TokenType.no);
          _keyword(TokenType.action);
          break;
      }
    }

    if (e.onUpdate != null) {
      _keyword(TokenType.on);
      _keyword(TokenType.update);
      referenceAction(e.onUpdate);
    }
    if (e.onDelete != null) {
      _keyword(TokenType.on);
      _keyword(TokenType.delete);
      referenceAction(e.onDelete);
    }

    visitNullable(e.deferrable, arg);
  }

  @override
  void visitFrameSpec(FrameSpec e, void arg) {
    void frameBoundary(FrameBoundary boundary) {
      void precedingOrFollowing(bool preceding) {
        if (boundary.isUnbounded) {
          _keyword(TokenType.unbounded);
        } else {
          visit(boundary.offset, arg);
        }

        _keyword(preceding ? TokenType.preceding : TokenType.following);
      }

      if (boundary.isCurrentRow) {
        _keyword(TokenType.current);
        _keyword(TokenType.row);
      } else if (boundary.preceding) {
        precedingOrFollowing(true);
      } else {
        precedingOrFollowing(false);
      }
    }

    _keyword(const {
      FrameType.range: TokenType.range,
      FrameType.rows: TokenType.rows,
      FrameType.groups: TokenType.groups,
    }[e.type]);

    if (e.end != null) {
      _keyword(TokenType.between);
      frameBoundary(e.start);
      _keyword(TokenType.and);
      frameBoundary(e.end);
    } else {
      frameBoundary(e.start);
    }

    if (e.excludeMode != null) {
      _keyword(TokenType.exclude);
      switch (e.excludeMode) {
        case ExcludeMode.noOthers:
          _keyword(TokenType.no);
          _keyword(TokenType.others);
          break;
        case ExcludeMode.currentRow:
          _keyword(TokenType.current);
          _keyword(TokenType.row);
          break;
        case ExcludeMode.group:
          _keyword(TokenType.group);
          break;
        case ExcludeMode.ties:
          _keyword(TokenType.ties);
          break;
      }
    }
  }

  @override
  void visitFunction(FunctionExpression e, void arg) {
    _identifier(e.name);
    _symbol('(');
    visit(e.parameters, arg);
    _symbol(')');
  }

  @override
  void visitGroupBy(GroupBy e, void arg) {
    _keyword(TokenType.group);
    _keyword(TokenType.by);

    _join(e.by, ',');

    if (e.having != null) {
      _keyword(TokenType.having);
      visit(e.having, arg);
    }
  }

  @override
  void visitInExpression(InExpression e, void arg) {
    visit(e.left, arg);
    _keyword(TokenType.$is);
    if (e.not) {
      _keyword(TokenType.not);
    }

    visit(e.inside, arg);
  }

  @override
  void visitIndexedColumn(IndexedColumn e, void arg) {
    visit(e.expression, arg);
    _orderingMode(e.ordering);
  }

  @override
  void visitInsertStatement(InsertStatement e, void arg) {
    visitNullable(e.withClause, arg);

    final mode = e.mode;
    if (mode == InsertMode.insert) {
      _keyword(TokenType.insert);
    } else if (mode == InsertMode.replace) {
      _keyword(TokenType.replace);
    } else {
      _keyword(TokenType.insert);
      _keyword(TokenType.or);

      _keyword(const {
        InsertMode.insertOrReplace: TokenType.replace,
        InsertMode.insertOrRollback: TokenType.rollback,
        InsertMode.insertOrAbort: TokenType.abort,
        InsertMode.insertOrFail: TokenType.fail,
        InsertMode.insertOrIgnore: TokenType.ignore,
      }[mode]);
    }

    _keyword(TokenType.into);
    visit(e.table, arg);

    if (e.targetColumns.isNotEmpty) {
      _symbol('(', spaceBefore: true);
      _join(e.targetColumns, ',');
      _symbol(')', spaceAfter: true);
    }

    visit(e.source, arg);
    visitNullable(e.upsert, arg);
  }

  @override
  void visitInsertTriggerTarget(InsertTarget e, void arg) {
    _keyword(TokenType.insert);
  }

  @override
  void visitInvalidStatement(InvalidStatement e, void arg) {
    throw UnsupportedError(
        'InvalidStatement does not have a textual representation');
  }

  @override
  void visitIsExpression(IsExpression e, void arg) {
    visit(e.left, arg);
    _keyword(TokenType.$is);

    if (e.negated) {
      _keyword(TokenType.not);
    }
    visit(e.right, arg);
  }

  @override
  void visitIsNullExpression(IsNullExpression e, void arg) {
    visit(e.operand, arg);

    if (e.negated) {
      _keyword(TokenType.notNull);
    } else {
      _keyword(TokenType.isNull);
    }
  }

  @override
  void visitJoin(Join e, void arg) {
    if (e.operator == JoinOperator.comma) {
      _symbol(',');
    } else {
      if (e.natural) {
        _keyword(TokenType.natural);
      }

      switch (e.operator) {
        case JoinOperator.none:
          break;
        case JoinOperator.comma:
          throw AssertionError("Can't happen");
        case JoinOperator.left:
          _keyword(TokenType.left);
          break;
        case JoinOperator.leftOuter:
          _keyword(TokenType.left);
          _keyword(TokenType.outer);
          break;
        case JoinOperator.inner:
          _keyword(TokenType.inner);
          break;
        case JoinOperator.cross:
          _keyword(TokenType.cross);
          break;
      }
      _keyword(TokenType.join);
    }

    visit(e.query, null);

    final constraint = e.constraint;
    if (constraint is OnConstraint) {
      _keyword(TokenType.on);
      visit(constraint.expression, arg);
    } else if (constraint is UsingConstraint) {
      _keyword(TokenType.using);
      _symbol('(${constraint.columnNames.join(', ')})');
    }
  }

  @override
  void visitJoinClause(JoinClause e, void arg) {
    visit(e.primary, arg);
    visitList(e.joins, arg);
  }

  @override
  void visitLimit(Limit e, void arg) {
    _keyword(TokenType.limit);
    visit(e.count, arg);

    if (e.offset != null) {
      _keyword(TokenType.offset);
      visit(e.offset, arg);
    }
  }

  @override
  void visitMoorDeclaredStatement(DeclaredStatement e, void arg) {
    _identifier(e.identifier.name);

    if (e.parameters.isNotEmpty) {
      _symbol('(');
      _join(e.parameters, ',');
      _symbol(')');
    }

    if (e.as != null) {
      _keyword(TokenType.as);
      _identifier(e.as);
    }

    _symbol(':', spaceAfter: true);
    visit(e.statement, arg);
    _symbol(';');
  }

  @override
  void visitMoorFile(MoorFile e, void arg) {
    for (final stmt in e.statements) {
      visit(stmt, arg);
      _buffer.write('\n');
      _needsSpace = false;
    }
  }

  @override
  void visitMoorImportStatement(ImportStatement e, void arg) {
    _keyword(TokenType.import);
    _stringLiteral(e.importedFile);
    _symbol(';', spaceAfter: true);
  }

  @override
  void visitMoorStatementParameter(StatementParameter e, void arg) {
    if (e is VariableTypeHint) {
      visit(e.variable, arg);
      _keyword(TokenType.as);
      _symbol(e.typeName, spaceBefore: true, spaceAfter: true);
    } else if (e is DartPlaceholderDefaultValue) {
      _symbol('\$${e.variableName}', spaceAfter: true);
      _symbol('=', spaceBefore: true, spaceAfter: true);
      visit(e.defaultValue, arg);
    } else {
      throw AssertionError('Unknown StatementParameter: $e');
    }
  }

  @override
  void visitNamedVariable(ColonNamedVariable e, void arg) {
    // Note: The name already starts with the colon
    _symbol(e.name, spaceBefore: true, spaceAfter: true);
  }

  @override
  void visitNullLiteral(NullLiteral e, void arg) {
    _keyword(TokenType.$null);
  }

  @override
  void visitNumberedVariable(NumberedVariable e, void arg) {
    _symbol('?', spaceBefore: true, spaceAfter: e.explicitIndex == null);
    if (e.explicitIndex != null) {
      _symbol(e.explicitIndex.toString(), spaceAfter: true);
    }
  }

  @override
  void visitNumericLiteral(NumericLiteral e, void arg) {
    _symbol(e.value.toString(), spaceBefore: true, spaceAfter: true);
  }

  @override
  void visitOrderBy(OrderBy e, void arg) {
    _keyword(TokenType.order);
    _keyword(TokenType.by);
    _join(e.terms, ',');
  }

  void _orderingMode(OrderingMode mode) {
    if (mode != null) {
      _keyword(const {
        OrderingMode.ascending: TokenType.asc,
        OrderingMode.descending: TokenType.desc,
      }[mode]);
    }
  }

  @override
  void visitOrderingTerm(OrderingTerm e, void arg) {
    visit(e.expression, arg);
    _orderingMode(e.orderingMode);

    if (e.nulls != null) {
      _keyword(TokenType.nulls);

      _keyword(const {
        OrderingBehaviorForNulls.first: TokenType.first,
        OrderingBehaviorForNulls.last: TokenType.last,
      }[e.nulls]);
    }
  }

  @override
  void visitParentheses(Parentheses e, void arg) {
    _symbol('(');
    visit(e.expression, arg);
    _symbol(')');
  }

  @override
  void visitReference(Reference e, void arg) {
    var hasTable = false;
    if (e.tableName != null) {
      hasTable = true;
      _identifier(e.tableName, spaceAfter: false);
      _symbol('.');
    }

    _identifier(e.columnName, spaceAfter: true, spaceBefore: !hasTable);
  }

  @override
  void visitStarResultColumn(StarResultColumn e, void arg) {
    if (e.tableName != null) {
      _identifier(e.tableName);
      _symbol('.');
    }

    _symbol('*', spaceAfter: true, spaceBefore: e.tableName == null);
  }

  @override
  void visitMoorNestedStarResultColumn(NestedStarResultColumn e, void arg) {
    _identifier(e.tableName);
    _symbol('.**', spaceAfter: true);
  }

  @override
  void visitExpressionResultColumn(ExpressionResultColumn e, void arg) {
    visit(e.expression, arg);
    if (e.as != null) {
      _keyword(TokenType.as);
      _identifier(e.as);
    }
  }

  @override
  void visitSelectInsertSource(SelectInsertSource e, void arg) {
    visit(e.stmt, arg);
  }

  @override
  void visitSelectStatement(SelectStatement e, void arg) {
    visitNullable(e.withClause, arg);
    _keyword(TokenType.select);
    if (e.distinct) {
      _keyword(TokenType.distinct);
    }

    _join(e.columns, ',');

    if (e.from != null) {
      _keyword(TokenType.from);
      visit(e.from, arg);
    }
    _where(e.where);
    visitNullable(e.groupBy, arg);
    if (e.windowDeclarations.isNotEmpty) {
      _keyword(TokenType.window);

      var isFirst = true;
      for (final declaration in e.windowDeclarations) {
        if (!isFirst) {
          _symbol(',', spaceAfter: true);
        }

        _identifier(declaration.name);
        _keyword(TokenType.as);

        visit(declaration.definition, arg);
        isFirst = false;
      }
    }
    visitNullable(e.orderBy, arg);
    visitNullable(e.limit, arg);
  }

  @override
  void visitSelectStatementAsSource(SelectStatementAsSource e, void arg) {
    _symbol('(');
    visit(e.statement, arg);
    _symbol(')');

    if (e.as != null) {
      _keyword(TokenType.as);
      _identifier(e.as);
    }
  }

  @override
  void visitSetComponent(SetComponent e, void arg) {
    visit(e.column, arg);
    _symbol('=', spaceBefore: true, spaceAfter: true);
    visit(e.expression, arg);
  }

  @override
  void visitStarFunctionParameter(StarFunctionParameter e, void arg) {
    _symbol('*', spaceAfter: true);
  }

  @override
  void visitStringComparison(StringComparisonExpression e, void arg) {
    visit(e.left, arg);
    if (e.not) {
      _keyword(TokenType.not);
    }

    _keyword(e.operator.type);
    visit(e.right, arg);

    if (e.escape != null) {
      _keyword(TokenType.escape);
      visit(e.escape, arg);
    }
  }

  @override
  void visitStringLiteral(StringLiteral e, void arg) {
    _stringLiteral(e.value);
  }

  @override
  void visitSubQuery(SubQuery e, void arg) {
    _symbol('(', spaceBefore: true);
    visit(e.select, arg);
    _symbol(')', spaceAfter: true);
  }

  @override
  void visitTableConstraint(TableConstraint e, void arg) {
    if (e.name != null) {
      _keyword(TokenType.constraint);
      _identifier(e.name);
    }

    if (e is KeyClause) {
      if (e.isPrimaryKey) {
        _keyword(TokenType.primary);
        _keyword(TokenType.key);
      } else {
        _keyword(TokenType.unique);
      }

      _symbol('(');
      _join(e.indexedColumns, ',');
      _symbol(')');
      _conflictClause(e.onConflict);
    } else if (e is CheckTable) {
      _keyword(TokenType.check);
      _symbol('(');
      visit(e.expression, arg);
      _symbol(')');
    } else if (e is ForeignKeyTableConstraint) {
      _keyword(TokenType.foreign);
      _keyword(TokenType.key);
      _symbol('(');
      _join(e.columns, ',');
      _symbol(')');
      visit(e.clause, arg);
    }
  }

  @override
  void visitTableReference(TableReference e, void arg) {
    _identifier(e.tableName);
    if (e.as != null) {
      _keyword(TokenType.as);
      _identifier(e.as);
    }
  }

  @override
  void visitTableValuedFunction(TableValuedFunction e, void arg) {
    _identifier(e.name);
    _symbol('(');
    visit(e.parameters, arg);
    _symbol(')');

    if (e.as != null) {
      _keyword(TokenType.as);
      _identifier(e.as);
    }
  }

  @override
  void visitTimeConstantLiteral(TimeConstantLiteral e, void arg) {
    switch (e.kind) {
      case TimeConstantKind.currentTime:
        _keyword(TokenType.currentTime);
        break;
      case TimeConstantKind.currentDate:
        _keyword(TokenType.currentDate);
        break;
      case TimeConstantKind.currentTimestamp:
        _keyword(TokenType.currentTimestamp);
        break;
    }
  }

  @override
  void visitTuple(Tuple e, void arg) {
    _symbol('(', spaceBefore: true);
    _join(e.expressions, ',');
    _symbol(')', spaceAfter: true);
  }

  @override
  void visitUnaryExpression(UnaryExpression e, void arg) {
    switch (e.operator.type) {
      case TokenType.minus:
        _symbol('-', spaceBefore: true);
        break;
      case TokenType.plus:
        _symbol('+', spaceBefore: true);
        break;
      case TokenType.tilde:
        _symbol('~', spaceBefore: true);
        break;
      case TokenType.not:
        _keyword(TokenType.not);
        break;
      default:
        throw AssertionError('Unknown unary operator: ${e.operator}');
    }

    visit(e.inner, arg);
  }

  @override
  void visitUpdateStatement(UpdateStatement e, void arg) {
    visitNullable(e.withClause, arg);
    _keyword(TokenType.update);

    if (e.or != null) {
      _keyword(TokenType.or);

      _keyword(const {
        FailureMode.rollback: TokenType.rollback,
        FailureMode.abort: TokenType.abort,
        FailureMode.replace: TokenType.replace,
        FailureMode.fail: TokenType.fail,
        FailureMode.ignore: TokenType.ignore,
      }[e.or]);
    }

    visit(e.table, arg);
    _keyword(TokenType.set);
    _join(e.set, ',');
    _where(e.where);
  }

  @override
  void visitUpdateTriggerTarget(UpdateTarget e, void arg) {
    _keyword(TokenType.update);
    if (e.columnNames.isNotEmpty) {
      _keyword(TokenType.of);
      _join(e.columnNames, ',');
    }
  }

  @override
  void visitUpsertClause(UpsertClause e, void arg) {
    _keyword(TokenType.on);
    _keyword(TokenType.conflict);

    if (e.onColumns != null) {
      _join(e.onColumns, ',');
      _where(e.where);
    }

    _keyword(TokenType.$do);
    visit(e.action, arg);
  }

  @override
  void visitValuesSelectStatement(ValuesSelectStatement e, void arg) {
    _keyword(TokenType.$values);
    _join(e.values, ',');
  }

  @override
  void visitValuesSource(ValuesSource e, void arg) {
    _keyword(TokenType.$values);
    _join(e.values, ',');
  }

  @override
  void visitWhen(WhenComponent e, void arg) {
    _keyword(TokenType.when);
    visit(e.when, arg);
    _keyword(TokenType.then);
    visit(e.then, arg);
  }

  @override
  void visitWindowDefinition(WindowDefinition e, void arg) {
    _symbol('(', spaceBefore: true);

    if (e.baseWindowName != null) {
      _identifier(e.baseWindowName);
    }

    if (e.partitionBy.isNotEmpty) {
      _keyword(TokenType.partition);
      _keyword(TokenType.by);
      _join(e.partitionBy, ',');
    }

    visitNullable(e.orderBy, arg);
    visitNullable(e.frameSpec, arg);

    _symbol(')', spaceAfter: true);
  }

  @override
  void visitWithClause(WithClause e, void arg) {
    _keyword(TokenType.$with);
    if (e.recursive) {
      _keyword(TokenType.recursive);
    }

    _join(e.ctes, ',');
  }
}
