// Copyright (c) 2017, Google Inc. Please see the AUTHORS file for details.
// All rights reserved. Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

import 'package:collection/collection.dart';

/// A JSON value.
///
/// This class is suitable for use in built_value fields. When serialized it
/// maps directly onto JSON values.
///
/// Deep operator== and hashCode are provided, meaning the contents of a
/// List or Map is used for equality and hashing.
///
/// List and Map classes are wrapped in [UnmodifiableListView] and
/// [UnmodifiableMapView] so they won't be modifiable via this object. You
/// must ensure that no updates are made via the original reference, as a
/// copy is not made.
///
/// Note: this is an experimental feature. API may change without a major
/// version increase.
abstract class JsonObject {
  /// The value, which may be a bool, a List, a Map, a num or a String.
  Object get value;

  /// Whether the value is a [bool].
  bool get isBool => false;

  /// The value as a [bool], or throw if not.
  bool get asBool => throw StateError('Not a bool.');

  /// Whether the value is a [List].
  bool get isList => false;

  /// The value as a [List], or throw if not.
  List get asList => throw StateError('Not a List.');

  /// Whether the value is a [Map].
  bool get isMap => false;

  /// The value as a [Map], or throw if not.
  Map get asMap => throw StateError('Not a Map.');

  /// Whether the value is a [num].
  bool get isNum => false;

  /// The value as a [num], or throw if not.
  num get asNum => throw StateError('Not a num.');

  /// Whether the value is a [String].
  bool get isString => false;

  /// The value as a [String], or throw if not.
  String get asString => throw StateError('Not a String.');

  /// Instantiates with [value], which must be a bool, a List, a Map, a num
  /// or a String. Otherwise, an [ArgumentError] is thrown.
  factory JsonObject(Object? value) {
    if (value is num) {
      return NumJsonObject(value);
    } else if (value is String) {
      return StringJsonObject(value);
    } else if (value is bool) {
      return BoolJsonObject(value);
    } else if (value is List<Object?>) {
      return ListJsonObject(value);
    } else if (value is Map<String, Object?>) {
      return MapJsonObject(value);
    } else if (value is Map) {
      // Allow wrong type map, check individual values.
      return MapJsonObject(value.cast());
    } else {
      throw ArgumentError.value(value, 'value',
          'Must be bool, List<Object?>, Map<String?, Object?>, num or String');
    }
  }

  JsonObject._();

  @override
  String toString() {
    return value.toString();
  }
}

/// A [JsonObject] holding a bool.
class BoolJsonObject extends JsonObject {
  @override
  final bool value;

  BoolJsonObject(this.value) : super._();

  @override
  bool get isBool => true;

  @override
  bool get asBool => value;

  @override
  bool operator ==(Object other) {
    if (identical(other, this)) return true;
    if (other is! BoolJsonObject) return false;
    return value == other.value;
  }

  @override
  int get hashCode => value.hashCode;
}

/// A [JsonObject] holding a List.
class ListJsonObject extends JsonObject {
  @override
  final List<Object?> value;

  ListJsonObject(List<Object?> value)
      : value = UnmodifiableListView<Object?>(value),
        super._();

  @override
  bool get isList => true;

  @override
  List<Object?> get asList => value;

  @override
  bool operator ==(Object other) {
    if (identical(other, this)) return true;
    if (other is! ListJsonObject) return false;
    return const DeepCollectionEquality().equals(value, other.value);
  }

  @override
  int get hashCode => const DeepCollectionEquality().hash(value);
}

/// A [JsonObject] holding a Map.
class MapJsonObject extends JsonObject {
  @override
  final Map<String, Object?> value;

  MapJsonObject(Map<String, Object?> value)
      : value = UnmodifiableMapView(value),
        super._();

  @override
  bool get isMap => true;

  @override
  Map<String, Object?> get asMap => value;

  @override
  bool operator ==(Object other) {
    if (identical(other, this)) return true;
    if (other is! MapJsonObject) return false;
    return const DeepCollectionEquality().equals(value, other.value);
  }

  @override
  int get hashCode => const DeepCollectionEquality().hash(value);
}

/// A [JsonObject] holding a num.
class NumJsonObject extends JsonObject {
  @override
  final num value;

  NumJsonObject(this.value) : super._();

  @override
  bool get isNum => true;

  @override
  num get asNum => value;

  @override
  bool operator ==(Object other) {
    if (identical(other, this)) return true;
    if (other is! NumJsonObject) return false;
    return value == other.value;
  }

  @override
  int get hashCode => value.hashCode;
}

/// A [JsonObject] holding a String.
class StringJsonObject extends JsonObject {
  @override
  final String value;

  StringJsonObject(this.value) : super._();

  @override
  bool get isString => true;

  @override
  String get asString => value;

  @override
  bool operator ==(Object other) {
    if (identical(other, this)) return true;
    if (other is! StringJsonObject) return false;
    return value == other.value;
  }

  @override
  int get hashCode => value.hashCode;
}
