import 'package:moor_generator/moor_generator.dart';
import 'package:recase/recase.dart';
import 'package:sqlparser/sqlparser.dart';

const _infoVersion = '0.1.0-dev-preview';

/// Utilities to transform moor schema entities to json.
class SchemaWriter {
  /// The parsed and resolved database for which the schema should be written.
  final Database db;

  final Map<MoorSchemaEntity, int> _entityIds = {};
  int _maxId = 0;

  SchemaWriter(this.db);

  int _idOf(MoorSchemaEntity entity) {
    return _entityIds.putIfAbsent(entity, () => _maxId++);
  }

  Map<String, dynamic> createSchemaJson() {
    return {
      '_meta': {
        'description': 'This file contains a serialized version of schema '
            'entities for moor.',
        'version': _infoVersion,
      },
      'entities': [
        for (final entity in db.entities) _entityToJson(entity),
      ],
    };
  }

  Map _entityToJson(MoorSchemaEntity entity) {
    String type;
    Map data;

    if (entity is MoorTable) {
      type = 'table';
      data = _tableData(entity);
    } else if (entity is MoorTrigger) {
      type = 'trigger';
      data = {
        'on': _idOf(entity.on),
        'refences_in_body': [
          for (final ref in entity.bodyReferences) _idOf(ref),
        ],
        'name': entity.displayName,
        'sql': entity.create,
      };
    } else if (entity is MoorIndex) {
      type = 'index';
      data = {
        'on': _idOf(entity.table),
        'name': entity.name,
        'sql': entity.createStmt,
      };
    } else if (entity is SpecialQuery) {
      type = 'special-query';
      data = {
        'scenario': 'create',
        'sql': entity.sql,
      };
    }

    return {
      'id': _idOf(entity),
      'references': [
        for (final reference in entity.references) _idOf(reference),
      ],
      'type': type,
      'data': data,
    };
  }

  Map _tableData(MoorTable table) {
    return {
      'name': table.sqlName,
      'was_declared_in_moor': table.isFromSql,
      'columns': [for (final column in table.columns) _columnData(column)],
      'is_virtual': table.isVirtualTable,
      if (table.isVirtualTable) 'create_virtual_stmt': table.createVirtual,
      if (table.overrideWithoutRowId != null)
        'without_rowid': table.overrideWithoutRowId,
      if (table.overrideTableConstraints != null)
        'constraints': table.overrideTableConstraints,
      if (table.primaryKey != null)
        'explicit_pk': [...table.primaryKey.map((c) => c.name.name)]
    };
  }

  Map _columnData(MoorColumn column) {
    return {
      'name': column.name.name,
      'moor_type': column.type.toString(),
      'nullable': column.nullable,
      'customConstraints': column.customConstraints,
      'default_dart': column.defaultArgument,
      'default_client_dart': column.clientDefaultCode,
      'dsl_features': [...column.features.map(_dslFeatureData)],
      if (column.typeConverter != null)
        'type_converter': {
          'dart_expr': column.typeConverter.expression,
          'dart_type_name': column.typeConverter.mappedType
              .getDisplayString(withNullability: false),
        }
    };
  }

  dynamic _dslFeatureData(ColumnFeature feature) {
    if (feature is AutoIncrement) {
      return 'auto-increment';
    } else if (feature is PrimaryKey) {
      return 'primary-key';
    } else if (feature is LimitingTextLength) {
      return {
        'allowed-lengths': {
          'min': feature.minLength,
          'max': feature.maxLength,
        },
      };
    }
    return 'unknown';
  }
}

/// Reads files generated by [SchemaWriter].
class SchemaReader {
  final Map<int, MoorSchemaEntity> _entitiesById = {};
  final Map<int, Map<String, dynamic>> _rawById = {};

  final Set<int> _currentlyProcessing = {};

  final SqlEngine _engine = SqlEngine();

  SchemaReader._();

  factory SchemaReader.readJson(Map<String, dynamic> json) {
    return SchemaReader._().._read(json);
  }

  Iterable<MoorSchemaEntity> get entities => _entitiesById.values;

  void _read(Map<String, dynamic> json) {
    final entities = json['entities'] as List<dynamic>;

    for (final raw in entities) {
      final rawData = raw as Map<String, dynamic>;
      final id = rawData['id'] as int;

      _rawById[id] = rawData;
    }

    _rawById.keys.forEach(_processById);
  }

  T _existingEntity<T extends MoorSchemaEntity>(dynamic id) {
    return _entitiesById[id as int] as T;
  }

  void _processById(int id) {
    if (_entitiesById.containsKey(id)) return;
    if (_currentlyProcessing.contains(id)) {
      throw ArgumentError(
          'Could not read schema file: Contains circular references.');
    }

    _currentlyProcessing.add(id);

    final rawData = _rawById[id];
    final references = (rawData['references'] as List<dynamic>).cast<int>();

    // Ensure that dependencies have been resolved
    references.forEach(_processById);

    final content = rawData['data'] as Map<String, dynamic>;
    final type = rawData['type'] as String;

    MoorSchemaEntity entity;
    switch (type) {
      case 'index':
        entity = _readIndex(content);
        break;
      case 'trigger':
        entity = _readTrigger(content);
        break;
      case 'table':
        entity = _readTable(content);
        break;
      case 'special-query':
        // Not relevant for the schema.
        return;
      default:
        throw ArgumentError(
            'Could not read schema file: Unknown entity $rawData');
    }

    _entitiesById[id] = entity;
  }

  MoorIndex _readIndex(Map<String, dynamic> content) {
    final on = _existingEntity<MoorTable>(content['on']);
    final name = content['name'] as String;
    final sql = content['sql'] as String;

    return MoorIndex(name, null, sql, on);
  }

  MoorTrigger _readTrigger(Map<String, dynamic> content) {
    final on = _existingEntity<MoorTable>(content['on']);
    final name = content['name'] as String;
    final sql = content['sql'] as String;

    final trigger = MoorTrigger(name, CustomTriggerDeclaration(sql), on);
    for (final bodyRef in content['refences_in_body'] as List) {
      trigger.bodyReferences.add(_existingEntity(bodyRef));
    }
    return trigger;
  }

  MoorTable _readTable(Map<String, dynamic> content) {
    final sqlName = content['name'] as String;
    final isVirtual = content['is_virtual'] as bool;
    final withoutRowId = content['without_rowid'] as bool /*?*/;

    if (isVirtual) {
      final create = content['create_virtual_stmt'] as String;
      final parsed =
          _engine.parse(create).rootNode as CreateVirtualTableStatement;

      return MoorTable(
        sqlName: sqlName,
        overriddenName: sqlName,
        declaration: CustomVirtualTableDeclaration(parsed),
        overrideWithoutRowId: withoutRowId,
      );
    }

    final columns = [
      for (final rawColumn in content['columns'] as List)
        _readColumn(rawColumn as Map<String, dynamic>)
    ];

    List<String> tableConstraints;
    if (content.containsKey('constraints')) {
      tableConstraints = (content['constraints'] as List<dynamic>).cast();
    }

    Set<MoorColumn> explicitPk;
    if (content.containsKey('explicit_pk')) {
      explicitPk = {
        for (final columnName in content['explicit_pk'] as List<dynamic>)
          columns.singleWhere((c) => c.name.name == columnName)
      };
    }

    return MoorTable(
      sqlName: sqlName,
      overriddenName: '_${ReCase(sqlName).pascalCase}',
      columns: columns,
      primaryKey: explicitPk,
      overrideTableConstraints: tableConstraints,
      overrideWithoutRowId: withoutRowId,
      declaration: const CustomTableDeclaration(),
    );
  }

  MoorColumn _readColumn(Map<String, dynamic> data) {
    final name = data['name'] as String;
    final moorType = ColumnType.values
        .firstWhere((type) => type.toString() == data['moor_type']);
    final nullable = data['nullable'] as bool;
    final customConstraints = data['customConstraints'] as String;
    final dslFeatures = [
      for (final feature in data['dsl_features'] as List<dynamic>)
        _columnFeature(feature)
    ];

    return MoorColumn(
      name: ColumnName.explicitly(name),
      dartGetterName: ReCase(name).camelCase,
      type: moorType,
      nullable: nullable,
      customConstraints: customConstraints,
      features: dslFeatures,
    );
  }

  ColumnFeature _columnFeature(dynamic data) {
    if (data == 'auto-increment') return AutoIncrement();
    if (data == 'primary-key') return const PrimaryKey();

    if (data is Map<String, dynamic>) {
      final allowedLengths = data['allowed-lengths'] as Map<String, dynamic>;
      return LimitingTextLength(
        minLength: allowedLengths['min'] as int,
        maxLength: allowedLengths['max'] as int,
      );
    }

    return null;
  }
}
