class Version implements Comparable<Version> {
  final int major;
  final int minor;
  final int? build;
  final int? revision;

  Version(
    this.major,
    this.minor, [
    this.build,
    this.revision,
  ]);

  Version incrementMajor() => Version(major + 1, 0);

  Version incrementMinor() => Version(major, minor + 1);

  Version incrementBuild() => Version(major, minor, (build ?? 0) + 1);

  Version incrementRevision() =>
      Version(major, minor, build, (revision ?? 0) + 1);

  static Version parse(String input) {
    final str = input.trim();
    if (str.isEmpty) {
      throw const FormatException("Cannot parse empty string into version.");
    }
    final segments = str.split('.');
    if (segments.length < 2) {
      throw const FormatException(
          "Format error: major and minor are required.");
    }
    if (segments.any((e) => !_isIntger(e))) {
      throw const FormatException("Format error: segment can only be intger.");
    }
    final len = segments.length;
    int _major = int.parse(segments[0]);
    int _minor = int.parse(segments[1]);
    int? _build;
    int? _revison;
    if (len > 2) _build = int.parse(segments[2]);
    if (len > 3) _revison = int.parse(segments[3]);
    return Version(_major, _minor, _build, _revison);
  }

  @override
  int compareTo(Version? other) {
    if (other == null) {
      throw ArgumentError.notNull("other");
    }

    return _compare(this, other);
  }

  @override
  String toString() {
    final _build = build ?? (revision == null ? null : 0);
    return <int?>[major, minor, _build, revision]
        .where((e) => e != null)
        .join('.');
  }

  @override
  bool operator ==(Object other) =>
      other is Version && _compare(this, other) == 0;
  bool operator <(dynamic o) => o is Version && _compare(this, o) < 0;
  bool operator <=(dynamic o) => o is Version && _compare(this, o) <= 0;
  bool operator >(dynamic o) => o is Version && _compare(this, o) > 0;
  bool operator >=(dynamic o) => o is Version && _compare(this, o) >= 0;

  @override
  int get hashCode => toString().hashCode;

  static int _compare(Version? a, Version? b) {
    if (a == null) {
      throw ArgumentError.notNull("a");
    }

    if (b == null) {
      throw ArgumentError.notNull("b");
    }

    if (a.major > b.major) return 1;
    if (a.major < b.major) return -1;

    if (a.minor > b.minor) return 1;
    if (a.minor < b.minor) return -1;

    if (b.build != null) {
      if (a.build != null) {
        if (a.build! > b.build!) return 1;
        if (a.build! < b.build!) return -1;
      } else {
        if (b.build! > 0 && b.revision != null && b.revision! > 0) {
          return -1;
        }
      }
    }
    if (b.revision != null) {
      if (a.revision != null) {
        if (a.revision! > b.revision!) return 1;
        if (a.revision! < b.revision!) return -1;
      } else {
        if (b.revision! > 0) {
          return -1;
        }
      }
    }
    return 0;
  }

  static bool _isIntger(String? s) {
    if (s == null) {
      return false;
    }
    return int.tryParse(s) != null;
  }
}
