import 'dart:collection';

import 'package:flutter/widgets.dart';
import 'package:json_annotation/json_annotation.dart';

part 'warband_roster.g.dart';

abstract class Unit {
  static List<String> _splitListFromJson(String list) {
    if (list == null) {
      return [];
    }
    return list.split(new RegExp(r" *, *"));
  }

  static String _joinListToJson(List<String> list) {
    return list.join(', ');
  }

  static Stats _statsFromJson(String stats) {
    RegExp re = new RegExp(
        r"\s*M([0-9]+[dD]*[6]*)\s*,\s*WS([0-9]+)\s*,\s*BS([0-9]+)\s*,\s*S([0-9]+)\s*,\s*T([0-9]+)\s*,\s*W([0-9]+)\s*,\s*I([0-9]+)\s*,\s*A([0-9]+)\s*,\s*Ld([0-9]+)\s*,\s*Sv([0-9\-]+)\s*");
    var matchList = re.allMatches(stats).toList();

    if (matchList.isEmpty) {
      throw FormatException(
          'The stats "' + stats + '" are not in the right format.');
    }

    var matches = matchList.first;
    return Stats(
        int.tryParse(matches.group(1)) ?? 0,
        int.tryParse(matches.group(2)) ?? 0,
        int.tryParse(matches.group(3)) ?? 0,
        int.tryParse(matches.group(4)) ?? 0,
        int.tryParse(matches.group(5)) ?? 0,
        int.tryParse(matches.group(6)) ?? 0,
        int.tryParse(matches.group(7)) ?? 0,
        int.tryParse(matches.group(8)) ?? 0,
        int.tryParse(matches.group(9)) ?? 0,
        int.tryParse(matches.group(10)) ?? 0);
  }

  static String _statsToJson(Stats stats) {
    return 'M${stats.movement}, WS${stats.weaponSkill}, BS${stats.ballisticSkill}, S${stats.strength}, T${stats.toughtness}, W${stats.wounds}, I${stats.initiative}, A${stats.attacks}, Ld${stats.leadership}, Sv${stats.save}';
  }
}

@JsonSerializable(nullable: true, anyMap: true)
class HenchmenGroup extends Unit {
  @JsonKey(
      name: 'group',
      fromJson: _henchmenHeaderFromJson,
      toJson: _henchmengroupHeaderToJson,
      required: true)
  final HashMap<String, String> header;
  @JsonKey(ignore: true)
  String name;
  @JsonKey(ignore: true)
  String type;
  @JsonKey(ignore: true)
  int number;
  @JsonKey(ignore: true)
  int experience;

  @JsonKey(defaultValue: false)
  bool large;

  @JsonKey(defaultValue: false)
  bool slowWitted;

  @JsonKey(defaultValue: false)
  bool mount;

  @JsonKey(name: 'attackanimal', defaultValue: false)
  bool attackAnimal;

  @JsonKey(
      fromJson: Unit._statsFromJson, toJson: Unit._statsToJson, required: true)
  final Stats stats;

  @JsonKey(fromJson: Unit._splitListFromJson, toJson: Unit._joinListToJson)
  final List<String> weapons;

  @JsonKey(fromJson: Unit._splitListFromJson, toJson: Unit._joinListToJson)
  final List<String> armour;

  @JsonKey(fromJson: Unit._splitListFromJson, toJson: Unit._joinListToJson)
  final List<String> rules;

  HenchmenGroup(this.header, this.stats, this.weapons, this.armour, this.rules,
      this.large, this.slowWitted, this.mount, this.attackAnimal) {
    this.name = this.header['name'];
    this.type = this.header['type'];
    this.number = int.tryParse(this.header['number']) ?? 1;
    this.experience = int.tryParse(this.header['experience']) ?? 0;
  }

  static HashMap<String, String> _henchmenHeaderFromJson(String header) {
    HashMap<String, String> h = new HashMap();
    RegExp re =
        new RegExp(r"([^\(]+)\(([0-9]+)x?\s+([^\)]+)\)\s*\[([0-9]+)XP\]\s*");
    var matchList = re.allMatches(header).toList();

    if (matchList.isEmpty) {
      throw FormatException('The henchmen group header "' +
          header +
          '" is not in the right format.');
    }

    var matches = matchList.first;

    h['name'] = matches.group(1).trim();
    h['number'] = matches.group(2);
    h['type'] = matches.group(3).trim();
    h['experience'] = matches.group(4);

    return h;
  }

  static String _henchmengroupHeaderToJson(
      HashMap<String, String> henchmenGroup) {
    return '${henchmenGroup['name']} (${henchmenGroup['number']} ${henchmenGroup['type']}) [${henchmenGroup['experience']}XP]';
  }

  factory HenchmenGroup.fromJson(yaml) => _$HenchmenGroupFromJson(yaml);
  Map<String, dynamic> toJson() => _$HenchmenGroupToJson(this);
}

@JsonSerializable(nullable: true, anyMap: true)
class Hero extends Unit {
  @JsonKey(
      name: 'hero',
      fromJson: _heroHeaderFromJson,
      toJson: _heroHeaderToJson,
      required: true)
  final HashMap<String, String> header;
  @JsonKey(ignore: true)
  String name;
  @JsonKey(ignore: true)
  String type;
  @JsonKey(ignore: true)
  int experience;

  @JsonKey(
      fromJson: Unit._statsFromJson, toJson: Unit._statsToJson, required: true)
  final Stats stats;

  @JsonKey(fromJson: Unit._splitListFromJson, toJson: Unit._joinListToJson)
  final List<String> skilllists;

  @JsonKey(fromJson: Unit._splitListFromJson, toJson: Unit._joinListToJson)
  final List<String> weapons;

  @JsonKey(fromJson: Unit._splitListFromJson, toJson: Unit._joinListToJson)
  final List<String> armour;

  @JsonKey(fromJson: Unit._splitListFromJson, toJson: Unit._joinListToJson)
  final List<String> rules;

  @JsonKey(fromJson: Unit._splitListFromJson, toJson: Unit._joinListToJson)
  final List<String> injuries;

  @JsonKey(defaultValue: 0)
  final int warbandaddition;
  @JsonKey(defaultValue: false)
  bool large;
  @JsonKey(defaultValue: false)
  bool slowWitted;

  @JsonKey(defaultValue: false, name: 'hiredsword')
  final bool hiredSword;

  @JsonKey(defaultValue: false, name: 'dramatispersonae')
  final bool dramatisPersonae;

  Hero(
      this.stats,
      this.skilllists,
      this.weapons,
      this.armour,
      this.rules,
      this.injuries,
      this.warbandaddition,
      this.large,
      this.header,
      this.hiredSword,
      this.dramatisPersonae,
      this.slowWitted) {
    this.name = this.header['name'];
    this.type = this.header['type'];
    this.experience = int.tryParse(this.header['experience']) ?? 0;
  }

  factory Hero.fromJson(yaml) => _$HeroFromJson(yaml);
  Map<String, dynamic> toJson() => _$HeroToJson(this);

  static HashMap<String, String> _heroHeaderFromJson(String header) {
    HashMap<String, String> h = new HashMap();
    RegExp re = new RegExp(r"([^\(]+)\(([^\)]+)\)\s*\[([0-9]+)XP\]\s*");
    var matchList = re.allMatches(header).toList();

    if (matchList.isEmpty) {
      throw FormatException(
          'The hero header "' + header + '" is not in the right format.');
    }

    var matches = matchList.first;

    h['name'] = matches.group(1).trim();
    h['type'] = matches.group(2).trim();
    h['experience'] = matches.group(3);

    return h;
  }

  static String _heroHeaderToJson(HashMap<String, String> heroHeader) {
    return '${heroHeader['name']} (${heroHeader['type']}) [${heroHeader['experience']}XP]';
  }
}

@immutable
class Stats {
  final int movement;
  final int weaponSkill;
  final int ballisticSkill;
  final int strength;
  final int toughtness;
  final int wounds;
  final int initiative;
  final int attacks;
  final int leadership;
  final int save;

  Stats(
      this.movement,
      this.weaponSkill,
      this.ballisticSkill,
      this.strength,
      this.toughtness,
      this.wounds,
      this.initiative,
      this.attacks,
      this.leadership,
      this.save);
}

class Version {
  String gitHash;
  String date;

  String author;
  String message;

  Version(this.gitHash, this.date, this.author, this.message);

  factory Version.fromJson(yaml) {
    return Version(
        yaml['gitHash'], yaml['date'], yaml['author'], yaml['message']);
  }
  Map<String, dynamic> toJson() {
    return {
      'gitHash': gitHash,
      'date': date,
      'author': author,
      'message': message
    };
  }
}

@JsonSerializable(nullable: true, anyMap: true)
class WarbandRoster {
  /// Store the complete string of name and race. This will split up into the
  /// fields name and race.
  @JsonKey(
      name: 'warband',
      fromJson: _warbandNameAndRaceFromJson,
      toJson: _warbandNameAndRaceToJson,
      required: true)
  final HashMap<String, String> nameAndRace;
  @JsonKey(ignore: true)
  String name;
  @JsonKey(ignore: true)
  String race;

  @JsonKey(name: 'active', defaultValue: true)
  bool active;

  @JsonKey(name: 'campaign', defaultValue: 0)
  final int campaignPoints;

  @JsonKey(required: true)
  final String objective;

  @JsonKey(required: true)
  final String alignment;

  @JsonKey(defaultValue: '')
  final String achievments;

  @JsonKey(defaultValue: 0)
  final int gc;

  @JsonKey(defaultValue: 0)
  final int shards;

  @JsonKey(defaultValue: '')
  final String equipment;

  @JsonKey(required: true)
  final List<Hero> heros;

  @JsonKey(name: 'henchmen', required: true)
  final List<HenchmenGroup> henchmenGroups;

  /// The players name is not defined in the yml file. This will be added later
  /// from the GitHubAdapter. Same goes for the lastSyncVersion and currentVersion.
  @JsonKey(required: false, defaultValue: 'Lonely Recluse')
  String playerName;

  @JsonKey(required: false)
  String filePath;

  @JsonKey(required: false)
  Version version;

  @JsonKey(required: false, defaultValue: true)
  bool unseen;

  WarbandRoster(
      this.nameAndRace,
      this.campaignPoints,
      this.objective,
      this.alignment,
      this.gc,
      this.shards,
      this.equipment,
      this.achievments,
      this.heros,
      this.henchmenGroups,
      this.playerName,
      this.filePath,
      this.version,
      this.unseen) {
    this.name = this.nameAndRace['name'];
    this.race = this.nameAndRace['race'];
  }

  int rating() {
    int rating = 0;

    heros.forEach((hero) {
      rating += hero.experience;
      rating += hero.warbandaddition;
      rating += 5;

      if (hero.large) {
        rating += 15;
      }
    });

    henchmenGroups.forEach((henchmenGroup) {
      rating += henchmenGroup.experience * henchmenGroup.number;
      rating += 5 * henchmenGroup.number;

      if (henchmenGroup.large) {
        rating += 15 * henchmenGroup.number;
      } else {
        if (henchmenGroup.mount || henchmenGroup.attackAnimal) {
          rating += 5 * henchmenGroup.number;
        }
      }
    });

    return rating;
  }

  int routLimit() {
    var unitCount = heros.length;
    henchmenGroups.forEach((group) => unitCount += group.number);

    var routLimit = (unitCount / 4).round();
    if (unitCount / 4 > routLimit) {
      routLimit++;
    }

    return routLimit;
  }

  static HashMap<String, String> _warbandNameAndRaceFromJson(
      String nameAndRace) {
    HashMap<String, String> nr = new HashMap();
    RegExp re = new RegExp(r"(.*) \((.*)\)");

    var matchList = re.allMatches(nameAndRace).toList();

    if (matchList.isEmpty) {
      throw FormatException(
          'Name and race "' + nameAndRace + '" are not in the right format.');
    }

    var matches = matchList.first;
    nr['name'] = matches.group(1).toString();
    nr['race'] = matches.group(2).toString();

    return nr;
  }

  static String _warbandNameAndRaceToJson(HashMap<String, String> nameAndRace) {
    return '${nameAndRace['name']} (${nameAndRace['race']})';
  }

  factory WarbandRoster.fromJson(yaml) => _$WarbandRosterFromJson(yaml);
  Map<String, dynamic> toJson() => _$WarbandRosterToJson(this);
}
