import 'package:dart_mappable/dart_mappable.dart';

import 'gist.dart';
import 'tutorial.dart';

part 'project.mapper.dart';

@MappableClass(discriminatorKey: 'type', generateMethods: GenerateMethods.decode, includeSubClasses: [TutorialData])
abstract class ProjectDataBase with ProjectDataBaseMappable {
  String? get id;
  String? get description;

  String? get htmlFile;
  String? get cssFile;
  String get mainDartFile;
  Map<String, String> get dartFiles;

  List<String> get fileNames;
  Map<String, String> get allDartFiles;

  bool isDart(String key);
  ProjectDataBase copy();
  String? fileContentFor(String key);
  ProjectDataBase updateContent(String key, String? content);

  Map<String, dynamic> toMap();
  String toJson();
}

@MappableClass()
class ProjectData with ProjectDataMappable implements ProjectDataBase {
  @override
  final String? id;
  @override
  final String? description;

  @override
  final String? htmlFile;
  @override
  final String? cssFile;
  @override
  final String mainDartFile;
  @override
  final Map<String, String> dartFiles;

  ProjectData({
    this.id,
    this.description,
    this.htmlFile,
    this.cssFile,
    required this.mainDartFile,
    this.dartFiles = const {},
  });

  factory ProjectData.fromGist(GistData gist) {
    var htmlFile = gist.files['index.html'];
    if (htmlFile != null && htmlFile.type != 'text/html') {
      print('[WARNING] index.html has wrong type "${htmlFile.type}"');
      htmlFile = null;
    }

    var cssFile = gist.files['styles.css'];
    if (cssFile != null && cssFile.type != 'text/css') {
      print('[WARNING] styles.css has wrong type "${cssFile.type}"');
      cssFile = null;
    }

    var dartFile = gist.files['main.dart'];
    if (dartFile == null) throw 'Missing main.dart file';
    if (dartFile.type != 'application/vnd.dart') throw 'main.dart has wrong type "${dartFile.type}"';

    var dartFiles = Map.fromEntries(gist.files.entries
        .where((e) => e.value.type == 'application/vnd.dart' && e.key != 'main.dart')
        .map((e) => MapEntry(e.key, e.value.content)));

    return ProjectData(
      id: 'gist-${gist.id}',
      description: gist.description,
      htmlFile: htmlFile?.content,
      cssFile: cssFile?.content,
      mainDartFile: dartFile.content,
      dartFiles: dartFiles,
    );
  }

  @override
  List<String> get fileNames =>
      ['main.dart', ...dartFiles.keys, if (htmlFile != null) 'index.html', if (cssFile != null) 'styles.css'];

  @override
  Map<String, String> get allDartFiles => {'main.dart': mainDartFile, ...dartFiles};

  @override
  ProjectData copy() => copyWith();

  @override
  String? fileContentFor(String key) {
    return switchFile(
      key,
      onMain: () => mainDartFile,
      onHtml: () => htmlFile,
      onCss: () => cssFile,
      onDart: () => dartFiles[key],
    );
  }

  @override
  ProjectData updateContent(String key, String? content) {
    return switchFile(
      key,
      onMain: () => copyWith(mainDartFile: content),
      onHtml: () => copyWith(htmlFile: content),
      onCss: () => copyWith(cssFile: content),
      onDart: () => copyWith(dartFiles: content != null ? {...dartFiles, key: content} : ({...dartFiles}..remove(key))),
    );
  }

  static T switchFile<T>(
    String key, {
    required T Function() onMain,
    required T Function() onHtml,
    required T Function() onCss,
    required T Function() onDart,
  }) {
    switch (key) {
      case 'main.dart':
        return onMain();
      case 'index.html':
        return onHtml();
      case 'styles.css':
        return onCss();
      default:
        return onDart();
    }
  }

  @override
  bool isDart(String key) {
    return key == 'main.dart' || dartFiles.keys.contains(key);
  }

  static bool canDelete(String key) {
    return switchFile(key, onMain: () => false, onHtml: () => true, onCss: () => true, onDart: () => true);
  }
}
