import 'dart:collection';
import 'package:angel_orm/angel_orm.dart';
import 'package:angel_migration/angel_migration.dart';
import 'package:charcode/ascii.dart';

abstract class PostgresGenerator {
  static String columnType(MigrationColumn column) {
    var str = column.type.name;
    if (column.length != null)
      return '$str(${column.length})';
    else
      return str;
  }

  static String compileColumn(MigrationColumn column) {
    var buf = new StringBuffer(columnType(column));

    if (column.isNullable == false) buf.write(' NOT NULL');
    if (column.defaultValue != null) {
      String s;
      var value = column.defaultValue;
      if (value is RawSql)
        s = value.value;
      else if (value is String) {
        var b = StringBuffer();
        for (var ch in value.codeUnits) {
          if (ch == $single_quote) {
            b.write("\\'");
          } else {
            b.writeCharCode(ch);
          }
        }
        s = b.toString();
      } else {
        s = value.toString();
      }

      buf.write(' DEFAULT $s');
    }

    if (column.indexType == IndexType.unique)
      buf.write(' UNIQUE');
    else if (column.indexType == IndexType.primaryKey)
      buf.write(' PRIMARY KEY');

    for (var ref in column.externalReferences) {
      buf.write(' ' + compileReference(ref));
    }

    return buf.toString();
  }

  static String compileReference(MigrationColumnReference ref) {
    var buf = new StringBuffer(
        'REFERENCES "${ref.foreignTable}"("${ref.foreignKey}")');
    if (ref.behavior != null) buf.write(' ' + ref.behavior);
    return buf.toString();
  }
}

class PostgresTable extends Table {
  final Map<String, MigrationColumn> _columns = {};

  @override
  MigrationColumn declareColumn(String name, Column column) {
    if (_columns.containsKey(name))
      throw new StateError('Cannot redeclare column "$name".');
    var col = new MigrationColumn.from(column);
    _columns[name] = col;
    return col;
  }

  void compile(StringBuffer buf, int indent) {
    int i = 0;

    _columns.forEach((name, column) {
      var col = PostgresGenerator.compileColumn(column);
      if (i++ > 0) buf.writeln(',');

      for (int i = 0; i < indent; i++) {
        buf.write('  ');
      }

      buf.write('"$name" $col');
    });
  }
}

class PostgresAlterTable extends Table implements MutableTable {
  final Map<String, MigrationColumn> _columns = {};
  final String tableName;
  final Queue<String> _stack = new Queue<String>();

  PostgresAlterTable(this.tableName);

  void compile(StringBuffer buf, int indent) {
    int i = 0;

    while (_stack.isNotEmpty) {
      var str = _stack.removeFirst();

      if (i++ > 0) buf.writeln(',');

      for (int i = 0; i < indent; i++) {
        buf.write('  ');
      }

      buf.write(str);
    }

    if (i > 0) buf.writeln(';');

    i = 0;
    _columns.forEach((name, column) {
      var col = PostgresGenerator.compileColumn(column);
      if (i++ > 0) buf.writeln(',');

      for (int i = 0; i < indent; i++) {
        buf.write('  ');
      }

      buf.write('ADD COLUMN "$name" $col');
    });
  }

  @override
  MigrationColumn declareColumn(String name, Column column) {
    if (_columns.containsKey(name))
      throw new StateError('Cannot redeclare column "$name".');
    var col = new MigrationColumn.from(column);
    _columns[name] = col;
    return col;
  }

  @override
  void dropNotNull(String name) {
    _stack.add('ALTER COLUMN "$name" DROP NOT NULL');
  }

  @override
  void setNotNull(String name) {
    _stack.add('ALTER COLUMN "$name" SET NOT NULL');
  }

  @override
  void changeColumnType(String name, ColumnType type, {int length}) {
    _stack.add('ALTER COLUMN "$name" TYPE ' +
        PostgresGenerator.columnType(
            new MigrationColumn(type, length: length)));
  }

  @override
  void renameColumn(String name, String newName) {
    _stack.add('RENAME COLUMN "$name" TO "$newName"');
  }

  @override
  void dropColumn(String name) {
    _stack.add('DROP COLUMN "$name"');
  }

  @override
  void rename(String newName) {
    _stack.add('RENAME TO "$newName"');
  }
}