import 'dart:async';
import 'annotations.dart';
import 'join_builder.dart';
import 'order_by.dart';
import 'query_base.dart';
import 'query_executor.dart';
import 'query_values.dart';
import 'query_where.dart';

/// A SQL `SELECT` query builder.
abstract class Query<T, Where extends QueryWhere> extends QueryBase<T> {
  final List<JoinBuilder> _joins = [];
  final Map<String, int> _names = {};
  final List<OrderBy> _orderBy = [];

  // An optional "parent query". If provided, [reserveName] will operate in
  // the parent's context.
  final Query parent;

  /// A map of field names to explicit SQL expressions. The expressions will be aliased
  /// to the given names.
  final Map<String, String> expressions = {};

  String _crossJoin, _groupBy;
  int _limit, _offset;

  Query({this.parent});

  Map<String, dynamic> get substitutionValues =>
      parent?.substitutionValues ?? super.substitutionValues;

  /// A reference to an abstract query builder.
  ///
  /// This is usually a generated class.
  Where get where;

  /// A set of values, for an insertion or update.
  ///
  /// This is usually a generated class.
  QueryValues get values;

  /// Preprends the [tableName] to the [String], [s].
  String adornWithTableName(String s) {
    if (expressions.containsKey(s)) {
      return '(${expressions[s]} AS $s)';
    } else {
      return '$tableName.$s';
    }
  }

  /// Returns a unique version of [name], which will not produce a collision within
  /// the context of this [query].
  String reserveName(String name) {
    if (parent != null) return parent.reserveName(name);
    var n = _names[name] ??= 0;
    _names[name]++;
    return n == 0 ? name : '${name}$n';
  }

  /// Makes a [Where] clause.
  Where newWhereClause() {
    throw UnsupportedError(
        'This instance does not support creating WHERE clauses.');
  }

  /// Determines whether this query can be compiled.
  ///
  /// Used to prevent ambiguities in joins.
  bool canCompile(Set<String> trampoline) => true;

  /// Shorthand for calling [where].or with a [Where] clause.
  void andWhere(void Function(Where) f) {
    var w = newWhereClause();
    f(w);
    where.and(w);
  }

  /// Shorthand for calling [where].or with a [Where] clause.
  void notWhere(void Function(Where) f) {
    var w = newWhereClause();
    f(w);
    where.not(w);
  }

  /// Shorthand for calling [where].or with a [Where] clause.
  void orWhere(void Function(Where) f) {
    var w = newWhereClause();
    f(w);
    where.or(w);
  }

  /// Limit the number of rows to return.
  void limit(int n) {
    _limit = n;
  }

  /// Skip a number of rows in the query.
  void offset(int n) {
    _offset = n;
  }

  /// Groups the results by a given key.
  void groupBy(String key) {
    _groupBy = key;
  }

  /// Sorts the results by a key.
  void orderBy(String key, {bool descending = false}) {
    _orderBy.add(OrderBy(key, descending: descending));
  }

  /// Execute a `CROSS JOIN` (Cartesian product) against another table.
  void crossJoin(String tableName) {
    _crossJoin = tableName;
  }

  String _joinAlias(Set<String> trampoline) {
    int i = _joins.length;

    while (true) {
      var a = 'a$i';
      if (trampoline.add(a)) {
        return a;
      } else {
        i++;
      }
    }
  }

  String Function() _compileJoin(tableName, Set<String> trampoline) {
    if (tableName is String) {
      return () => tableName;
    } else if (tableName is Query) {
      return () {
        var c = tableName.compile(trampoline);
        if (c == null) return c;
        return '($c)';
      };
    } else {
      throw ArgumentError.value(
          tableName, 'tableName', 'must be a String or Query');
    }
  }

  void _makeJoin(
      tableName,
      Set<String> trampoline,
      JoinType type,
      String localKey,
      String foreignKey,
      String op,
      List<String> additionalFields) {
    trampoline ??= Set();

    // Pivot tables guard against ambiguous fields by excluding tables
    // that have already been queried in this scope.
    if (trampoline.contains(tableName) && trampoline.contains(this.tableName)) {
      // ex. if we have {roles, role_users}, then don't join "roles" again.
      return;
    }

    var to = _compileJoin(tableName, trampoline);
    if (to != null) {
      var alias = _joinAlias(trampoline);
      if (tableName is Query) {
        for (var field in tableName.fields) {
          tableName.aliases[field] = '${alias}_$field';
        }
      }
      _joins.add(JoinBuilder(type, this, to, localKey, foreignKey,
          op: op,
          alias: alias,
          additionalFields: additionalFields,
          aliasAllFields: tableName is Query));
    }
  }

  /// Execute an `INNER JOIN` against another table.
  void join(tableName, String localKey, String foreignKey,
      {String op = '=',
      List<String> additionalFields = const [],
      Set<String> trampoline}) {
    _makeJoin(tableName, trampoline, JoinType.inner, localKey, foreignKey, op,
        additionalFields);
  }

  /// Execute a `LEFT JOIN` against another table.
  void leftJoin(tableName, String localKey, String foreignKey,
      {String op = '=',
      List<String> additionalFields = const [],
      Set<String> trampoline}) {
    _makeJoin(tableName, trampoline, JoinType.left, localKey, foreignKey, op,
        additionalFields);
  }

  /// Execute a `RIGHT JOIN` against another table.
  void rightJoin(tableName, String localKey, String foreignKey,
      {String op = '=',
      List<String> additionalFields = const [],
      Set<String> trampoline}) {
    _makeJoin(tableName, trampoline, JoinType.right, localKey, foreignKey, op,
        additionalFields);
  }

  /// Execute a `FULL OUTER JOIN` against another table.
  void fullOuterJoin(tableName, String localKey, String foreignKey,
      {String op = '=',
      List<String> additionalFields = const [],
      Set<String> trampoline}) {
    _makeJoin(tableName, trampoline, JoinType.full, localKey, foreignKey, op,
        additionalFields);
  }

  /// Execute a `SELF JOIN`.
  void selfJoin(tableName, String localKey, String foreignKey,
      {String op = '=',
      List<String> additionalFields = const [],
      Set<String> trampoline}) {
    _makeJoin(tableName, trampoline, JoinType.self, localKey, foreignKey, op,
        additionalFields);
  }

  @override
  String compile(Set<String> trampoline,
      {bool includeTableName = false,
      String preamble,
      bool withFields = true,
      String fromQuery}) {
    // One table MAY appear multiple times in a query.
    if (!canCompile(trampoline)) {
      return null;
    }

    includeTableName = includeTableName || _joins.isNotEmpty;
    var b = StringBuffer(preamble ?? 'SELECT');
    b.write(' ');
    List<String> f;

    var compiledJoins = <JoinBuilder, String>{};

    if (fields == null) {
      f = ['*'];
    } else {
      f = List<String>.from(fields.map((s) {
        var ss = includeTableName ? '$tableName.$s' : s;
        if (expressions.containsKey(s)) {
          // ss = '(' + expressions[s] + ')';
          ss = expressions[s];
        }
        var cast = casts[s];
        if (cast != null) ss = 'CAST ($ss AS $cast)';
        if (aliases.containsKey(s)) {
          if (cast != null) {
            ss = '($ss) AS ${aliases[s]}';
          } else {
            ss = '$ss AS ${aliases[s]}';
          }
          if (expressions.containsKey(s)) {
            ss = '($ss)';
          }
        } else if (expressions.containsKey(s)) {
          if (cast != null) {
            ss = '(($ss) AS $s)';
          } else {
            ss = '($ss AS $s)';
          }
        }
        return ss;
      }));
      _joins.forEach((j) {
        var c = compiledJoins[j] = j.compile(trampoline);
        if (c != null) {
          var additional = j.additionalFields.map(j.nameFor).toList();
          f.addAll(additional);
        } else {
          // If compilation failed, fill in NULL placeholders.
          for (var i = 0; i < j.additionalFields.length; i++) {
            f.add('NULL');
          }
        }
      });
    }
    if (withFields) b.write(f.join(', '));
    fromQuery ??= tableName;
    b.write(' FROM $fromQuery');

    // No joins if it's not a select.
    if (preamble == null) {
      if (_crossJoin != null) b.write(' CROSS JOIN $_crossJoin');
      for (var join in _joins) {
        var c = compiledJoins[join];
        if (c != null) b.write(' $c');
      }
    }

    var whereClause =
        where.compile(tableName: includeTableName ? tableName : null);
    if (whereClause.isNotEmpty) b.write(' WHERE $whereClause');
    if (_groupBy != null) b.write(' GROUP BY $_groupBy');
    for (var item in _orderBy) {
      b.write(' ORDER BY ${item.compile()}');
    }
    if (_limit != null) b.write(' LIMIT $_limit');
    if (_offset != null) b.write(' OFFSET $_offset');
    return b.toString();
  }

  @override
  Future<T> getOne(QueryExecutor executor) {
    //limit(1);
    return super.getOne(executor);
  }

  Future<List<T>> delete(QueryExecutor executor) {
    var sql = compile(Set(), preamble: 'DELETE', withFields: false);

    if (_joins.isEmpty) {
      return executor
          .query(tableName, sql, substitutionValues,
              fields.map(adornWithTableName).toList())
          .then((it) => it.map(deserialize).toList());
    } else {
      return executor.transaction((tx) async {
        // TODO: Can this be done with just *one* query?
        var existing = await get(tx);
        //var sql = compile(preamble: 'SELECT $tableName.id', withFields: false);
        return tx
            .query(tableName, sql, substitutionValues)
            .then((_) => existing);
      });
    }
  }

  Future<T> deleteOne(QueryExecutor executor) {
    return delete(executor).then((it) => it.isEmpty ? null : it.first);
  }

  Future<T> insert(QueryExecutor executor) {
    var insertion = values.compileInsert(this, tableName);

    if (insertion == null) {
      throw StateError('No values have been specified for update.');
    } else {
      // TODO: How to do this in a non-Postgres DB?
      var returning = fields.map(adornWithTableName).join(', ');
      var sql = compile(Set());
      sql = 'WITH $tableName as ($insertion RETURNING $returning) ' + sql;
      return executor
          .query(tableName, sql, substitutionValues)
          .then((it) => it.isEmpty ? null : deserialize(it.first));
    }
  }

  Future<List<T>> update(QueryExecutor executor) async {
    var updateSql = StringBuffer('UPDATE $tableName ');
    var valuesClause = values.compileForUpdate(this);

    if (valuesClause == null) {
      throw StateError('No values have been specified for update.');
    } else {
      updateSql.write(' $valuesClause');
      var whereClause = where.compile();
      if (whereClause.isNotEmpty) updateSql.write(' WHERE $whereClause');
      if (_limit != null) updateSql.write(' LIMIT $_limit');

      var returning = fields.map(adornWithTableName).join(', ');
      var sql = compile(Set());
      sql = 'WITH $tableName as ($updateSql RETURNING $returning) ' + sql;

      return executor
          .query(tableName, sql, substitutionValues)
          .then((it) => it.map(deserialize).toList());
    }
  }

  Future<T> updateOne(QueryExecutor executor) {
    return update(executor).then((it) => it.isEmpty ? null : it.first);
  }
}