@immutable
class SingleBalanceInfo extends BaseBalanceInfo {
  const SingleBalanceInfo({
    required this.spendable,
    required this.unspendable,
  });

  SingleBalanceInfo.zero()
      : spendable = Decimal.zero,
        unspendable = Decimal.zero;

  factory SingleBalanceInfo.fromJson(Map<String, dynamic> json) {
    final maybeTotal = json.valueOrNull<String>('balance');
    final maybeUnspendable = json.valueOrNull<String>('unspendable') ??
        json.valueOrNull<String>('unspendable_balance');
    final maybeSpendable = json.valueOrNull<String>('spendable') ??
        json.valueOrNull<String>('spendable_balance');

    return maybeTotal != null
        ? SingleBalanceInfo.fromTotal(
            Decimal.parse(maybeTotal),
            unspendable: maybeUnspendable != null
                ? Decimal.parse(maybeUnspendable)
                : null,
            spendable:
                maybeSpendable != null ? Decimal.parse(maybeSpendable) : null,
          )
        : SingleBalanceInfo(
            spendable: Decimal.parse(maybeSpendable!),
            unspendable: Decimal.parse(maybeUnspendable!),
          );
  }

  factory SingleBalanceInfo.fromTotal(
    Decimal total, {
    Decimal? unspendable,
    Decimal? spendable,
  }) {
    assert(
      [unspendable, spendable]
              .where((e) => e != null && e > Decimal.zero)
              .length <=
          1,
      'Only one can be greater than zero or non-null',
    );

    Decimal? spendableBalance;
    Decimal? unspendableBalance;

    spendableBalance = (spendable != null && spendable > Decimal.zero)
        ? spendable
        : total - (unspendable ?? Decimal.zero);

    unspendableBalance = (unspendable != null && unspendable > Decimal.zero)
        ? unspendable
        : total - (spendable ?? Decimal.zero);

    return SingleBalanceInfo(
      spendable: spendableBalance,
      unspendable: unspendableBalance,
    );
  }

  final Decimal spendable;
  final Decimal unspendable;

  Decimal get total => spendable + unspendable;

  @override
  bool get hasBalance => spendable > Decimal.zero || unspendable > Decimal.zero;

  @override
  Map<String, dynamic> toJson() {
    return {
      'spendable': spendable.toString(),
      'unspendable': unspendable.toString(),
    };
  }

  // Mathematical operators
  SingleBalanceInfo operator +(SingleBalanceInfo other) {
    return SingleBalanceInfo(
      spendable: spendable + other.spendable,
      unspendable: unspendable + other.unspendable,
    );
  }

  SingleBalanceInfo operator -(SingleBalanceInfo other) {
    return SingleBalanceInfo(
      spendable: spendable - other.spendable,
      unspendable: unspendable - other.unspendable,
    );
  }

  @override
  bool operator ==(Object other) {
    if (identical(this, other)) return true;
    return other is SingleBalanceInfo &&
        other.spendable == spendable &&
        other.unspendable == unspendable;
  }

  @override
  int get hashCode => spendable.hashCode ^ unspendable.hashCode;
}

/// Multi-coin balance information
@immutable
class MultiBalanceInfo extends BaseBalanceInfo {
  MultiBalanceInfo({required Map<String, SingleBalanceInfo> balances})
      : _balances = Map.unmodifiable(balances);

  factory MultiBalanceInfo.fromJson(Map<String, dynamic> json) {
    final balances = <String, SingleBalanceInfo>{};
    json.forEach((key, value) {
      if (value is Map<String, dynamic>) {
        balances[key] = SingleBalanceInfo.fromJson(value as Map<String, dynamic>);
      }
    });
    return MultiBalanceInfo(balances: balances);
  }

  final Map<String, SingleBalanceInfo> _balances;

  Map<String, SingleBalanceInfo> get balances => _balances;
  
  SingleBalanceInfo? operator [](String ticker) => _balances[ticker];

  @override
  bool get hasBalance => _balances.values.any((b) => b.hasBalance);

  @override
  Map<String, dynamic> toJson() {
    return _balances.map((key, value) => MapEntry(key, value.toJson()));
  }

  @override
  bool operator ==(Object other) {
    if (identical(this, other)) return true;
    return other is MultiBalanceInfo &&
        MapEquality().equals(_balances, other._balances);
  }

  @override
  int get hashCode => MapEquality().hash(_balances);
}
