import 'dart:io';

enum Difficulty { Beginner, Easy, Medium, Hard, Challenge, Edit }

// These are the standard note values:
//
//     0 – No note
//     1 – Normal note
//     2 – Hold head
//     3 – Hold/Roll tail
//     4 – Roll head
//     M – Mine (or other negative note)
//
// Later versions of StepMania accept other note values which may not work in older versions:
//
//     K – Automatic keysound
//     L – Lift note
//     F – Fake note

RegExp noteTypes = RegExp(r'^([012345MKLF]+)\s*([,;])?');

class Chart {
  String? chartType;
  // Description/author
  String? author;
  // Difficulty (one of Beginner, Easy, Medium, Hard, Challenge, Edit)
  Difficulty? difficulty;
  // Numerical meter
  int? numericalMeter;
  // Groove radar values, generated by the program
  String? radarValues;

  List<List<String>>? measures;

  Map<double, String> beats = {};
}

class Simfile {
  String directoryPath;
  String? simfilePath;
  String? audioPath;
  String? bannerPath;
  String? lines;

  // tags of simfile
  Map<String, String> tags = {};

  Chart? chartSimplest;

  Map<double, double> bpms = {};
  double offset = 0;

  Simfile(this.directoryPath);

  void _parseChart({required List<String> keys, required String value}) {
    Chart chart = Chart();
    chart.chartType = keys[1];
    chart.author = keys[2];
    chart.difficulty = Difficulty.values.byName(keys[3]);
    chart.numericalMeter = int.parse(keys[4]);
    chart.radarValues = keys[5];

    if (chartSimplest == null ||
        (chart.difficulty!.index <= chartSimplest!.difficulty!.index &&
            chart.numericalMeter! <= chartSimplest!.numericalMeter!)) {
      List<List<String>> measures = [];
      for (final measureRaw in value.split(',')) {
        List<String> measure = [];
        for (final noteRaw in measureRaw.split('\n')) {
          String note = noteRaw.trim();
          if (noteTypes.hasMatch(note)) {
            measure.add(note);
          }
        }
        measures.add(measure);
      }

      double bpm = bpms.entries.first.value;

      for (final (measureIndex, measure) in measures.indexed) {
        for (final (noteIndex, noteData) in measure.indexed) {
          double beat = measureIndex * 4.0 +
              (noteIndex.toDouble() / measure.length) * 4.0;
          double minutesPerBeat = 1.0 / bpm;
          double offsetMinutes = offset / 60.0;
          chart.beats[beat * minutesPerBeat + offsetMinutes] = noteData;
        }
      }

      chart.measures = measures;
      chartSimplest = chart;
    }
  }

  void _parseTag(RegExpMatch fieldData) {
    List<String> keys =
        fieldData[1]!.split(':').map((key) => key.trim()).toList();
    String value = fieldData[2]!;
    if (keys[0] == "BPMS") {
      for (final pairRaw in value.split(',')) {
        List<String> pair = pairRaw.split('=');
        if (pair.length != 2) {
          continue;
        }
        double time = double.parse(pair[0]);
        double bpm = double.parse(pair[1]);
        bpms[time] = bpm;
      }
    }

    if (keys[0] == "OFFSET") {
      offset = double.parse(value);
    }

    if (keys[0] != "NOTES") {
      tags[keys[0]] = value;
      return;
    }
    _parseChart(keys: keys, value: value);
  }

  void load() {
    simfilePath = Directory(directoryPath)
        .listSync()
        .firstWhere((entity) => entity.path.endsWith('.sm'),
            orElse: () => File(''))
        .path;

    audioPath = Directory(directoryPath)
        .listSync()
        .firstWhere((entity) => entity.path.endsWith('.ogg'),
            orElse: () => File(''))
        .path;

    bannerPath = Directory(directoryPath)
        .listSync()
        .firstWhere((file) => file.path.toLowerCase().endsWith('banner.png'),
            orElse: () => File(''))
        .path;

    lines = File(simfilePath!).readAsStringSync();

    RegExp commentsRegExp = RegExp(r'//.*$');
    lines = lines?.replaceAll(commentsRegExp, '');
    RegExp fieldDataRegExp = RegExp(r'#([^;]+):([^;]*);');

    for (final fieldData in fieldDataRegExp.allMatches(lines!)) {
      _parseTag(fieldData);
    }
  }
}
