// Copyright (c) 2017, the Dart project authors.  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:meta/meta_meta.dart';

import 'allowed_keys_helpers.dart';
import 'checked_helpers.dart';
import 'enum_helpers.dart';
import 'json_converter.dart';
import 'json_key.dart';

part 'json_serializable.g.dart';

/// Values for the automatic field renaming behavior for [JsonSerializable].
enum FieldRename {
  /// Use the field name without changes.
  none,

  /// Encodes a field named `kebabCase` with a JSON key `kebab-case`.
  kebab,

  /// Encodes a field named `snakeCase` with a JSON key `snake_case`.
  snake,

  /// Encodes a field named `pascalCase` with a JSON key `PascalCase`.
  pascal,

  /// Encodes a field named `screamingSnakeCase` with a JSON key
  /// `SCREAMING_SNAKE_CASE`
  screamingSnake,
}

/// An annotation used to specify a class to generate code for.
@JsonSerializable(
  checked: true,
  disallowUnrecognizedKeys: true,
  fieldRename: FieldRename.snake,
)
@Target({TargetKind.classType})
class JsonSerializable {
  /// If `true`, [Map] types are *not* assumed to be [Map<String, dynamic>]
  /// – which is the default type of [Map] instances return by JSON decode in
  /// `dart:convert`.
  ///
  /// This will increase the code size, but allows [Map] types returned
  /// from other sources, such as `package:yaml`.
  ///
  /// *Note: in many cases the key values are still assumed to be [String]*.
  final bool? anyMap;

  /// If `true`, generated `fromJson` functions include extra checks to validate
  /// proper deserialization of types.
  ///
  /// If an exception is thrown during deserialization, a
  /// [CheckedFromJsonException] is thrown.
  final bool? checked;

  /// Specifies a named constructor to target when creating the `fromJson`
  /// function.
  ///
  /// If the value is not set or an empty [String], the default constructor
  /// is used.
  ///
  /// This setting has no effect if [createFactory] is `false`.
  final String? constructor;

  /// If `true` (the default), a private, static `_$ExampleFromJson` method
  /// is created in the generated part file.
  ///
  /// Call this method from a factory constructor added to the source class:
  ///
  /// ```dart
  /// @JsonSerializable()
  /// class Example {
  ///   // ...
  ///   factory Example.fromJson(Map<String, dynamic> json) =>
  ///     _$ExampleFromJson(json);
  /// }
  /// ```
  final bool? createFactory;

  /// If `true` (defaults to false), a private, static `_$ExampleJsonMeta`
  /// constant is created in the generated part file.
  ///
  /// This constant can be used by other code-generators to support features
  /// such as [fieldRename].
  final bool? createFieldMap;

  /// If `true` (defaults to false), a private class `_$ExampleJsonKeys`
  /// class is created in the generated part file.
  ///
  /// This class will contain every property as a [String] field with the JSON
  /// key as the value.
  ///
  /// ```dart
  /// @JsonSerializable(createJsonKeys: true)
  /// class Example {
  ///   @JsonKey(name: 'LAST_NAME')
  ///   String? firstName;
  ///
  ///   // Will have the value `LAST_NAME`
  ///   static const firstName = _$ExampleJsonKeys.firstName;
  /// }
  /// ```
  final bool? createJsonKeys;

  /// If `true` (defaults to false), a private, static `_$ExamplePerFieldToJson`
  /// abstract class will be generated in the part file.
  ///
  /// This abstract class will contain one static function per property,
  /// exposing a way to encode only this property instead of the entire object.
  final bool? createPerFieldToJson;

  /// If `true` (the default), A top-level function is created that you can
  /// reference from your class.
  ///
  /// ```dart
  /// @JsonSerializable()
  /// class Example {
  ///   Map<String, dynamic> toJson() => _$ExampleToJson(this);
  /// }
  /// ```
  final bool? createToJson;

  /// If `false` (the default), then the generated `FromJson` function will
  /// ignore unrecognized keys in the provided JSON [Map].
  ///
  /// If `true`, unrecognized keys will cause an [UnrecognizedKeysException] to
  /// be thrown.
  final bool? disallowUnrecognizedKeys;

  /// If `true`, generated `toJson` methods will explicitly call `toJson` on
  /// nested objects.
  ///
  /// When using JSON encoding support in `dart:convert`, `toJson` is
  /// automatically called on objects, so the default behavior
  /// (`explicitToJson: false`) is to omit the `toJson` call.
  ///
  /// Example of `explicitToJson: false` (default)
  ///
  /// ```dart
  /// Map<String, dynamic> toJson() => {'child': child};
  /// ```
  ///
  /// Example of `explicitToJson: true`
  ///
  /// ```dart
  /// Map<String, dynamic> toJson() => {'child': child?.toJson()};
  /// ```
  final bool? explicitToJson;

  /// Defines the automatic naming strategy when converting class field names
  /// into JSON map keys.
  ///
  /// With a value [FieldRename.none] (the default), the name of the field is
  /// used without modification.
  ///
  /// See [FieldRename] for details on the other options.
  ///
  /// Note: the value for [JsonKey.name] takes precedence over this option for
  /// fields annotated with [JsonKey].
  final FieldRename? fieldRename;

  /// When `true` on classes with type parameters (generic types), extra
  /// "helper" parameters will be generated for `fromJson` and/or `toJson` to
  /// support serializing values of those types.
  ///
  /// For example, the generated code for
  ///
  /// ```dart
  /// @JsonSerializable(genericArgumentFactories: true)
  /// class Response<T> {
  ///   int status;
  ///   T value;
  /// }
  /// ```
  ///
  /// Looks like
  ///
  /// ```dart
  /// Response<T> _$ResponseFromJson<T>(
  ///   Map<String, dynamic> json,
  ///   T Function(Object json) fromJsonT,
  /// ) {
  ///   return Response<T>()
  ///     ..status = (json['status'] as num).toInt()
  ///     ..value = fromJsonT(json['value']);
  /// }
  ///
  /// Map<String, dynamic> _$ResponseToJson<T>(
  ///   Response<T> instance,
  ///   Object Function(T value) toJsonT,
  /// ) =>
  ///     <String, dynamic>{
  ///       'status': instance.status,
  ///       'value': toJsonT(instance.value),
  ///     };
  /// ```
  ///
  /// Notes:
  ///
  /// 1. This option has no effect on classes without type parameters.
  ///    If used on such a class, a warning is echoed in the build log.
  /// 1. If this option is set for all classes in a package via `build.yaml`
  ///    it is only applied to classes with type parameters – so no warning is
  ///    echoed.
  final bool? genericArgumentFactories;

  /// When `true`, only fields annotated with [JsonKey] will have code
  /// generated.
  ///
  /// It will have the same effect as if those fields had been annotated with
  /// [JsonKey.includeToJson] and [JsonKey.includeFromJson] set to `false`
  final bool? ignoreUnannotated;

  /// Whether the generator should include fields with `null` values in the
  /// serialized output.
  ///
  /// If `true` (the default), all fields are written to JSON, even if they are
  /// `null`.
  ///
  /// If a field is annotated with `JsonKey` with a non-`null` value for
  /// `includeIfNull`, that value takes precedent.
  final bool? includeIfNull;

  /// A list of [JsonConverter] to apply to this class.
  ///
  /// Writing:
  ///
  /// ```dart
  /// @JsonSerializable(converters: [MyJsonConverter()])
  /// class Example {...}
  /// ```
  ///
  /// is equivalent to writing:
  ///
  /// ```dart
  /// @JsonSerializable()
  /// @MyJsonConverter()
  /// class Example {...}
  /// ```
  ///
  /// The main difference is that this allows reusing a custom
  /// [JsonSerializable] over multiple classes:
  ///
  /// ```dart
  /// const myCustomAnnotation = JsonSerializable(
  ///   converters: [MyJsonConverter()],
  /// );
  ///
  /// @myCustomAnnotation
  /// class Example {...}
  ///
  /// @myCustomAnnotation
  /// class Another {...}
  /// ```
  @JsonKey(includeFromJson: false, includeToJson: false)
  final List<JsonConverter>? converters;

  /// Creates a new [JsonSerializable] instance.
  const JsonSerializable({
    @Deprecated('Has no effect') bool? nullable,
    this.anyMap,
    this.checked,
    this.constructor,
    this.createFieldMap,
    this.createJsonKeys,
    this.createFactory,
    this.createToJson,
    this.disallowUnrecognizedKeys,
    this.explicitToJson,
    this.fieldRename,
    this.ignoreUnannotated,
    this.includeIfNull,
    this.converters,
    this.genericArgumentFactories,
    this.createPerFieldToJson,
  });

  factory JsonSerializable.fromJson(Map<String, dynamic> json) =>
      _$JsonSerializableFromJson(json);

  /// An instance of [JsonSerializable] with all fields set to their default
  /// values.
  @Deprecated('Was only ever included to support builder infrastructure.')
  static const defaults = JsonSerializable(
    anyMap: false,
    checked: false,
    constructor: '',
    createFactory: true,
    createToJson: true,
    disallowUnrecognizedKeys: false,
    explicitToJson: false,
    fieldRename: FieldRename.none,
    ignoreUnannotated: false,
    includeIfNull: true,
    genericArgumentFactories: false,
  );

  /// Returns a new [JsonSerializable] instance with fields equal to the
  /// corresponding values in `this`, if not `null`.
  ///
  /// Otherwise, the returned value has the default value as defined in
  /// [defaults].
  @Deprecated('Was only ever included to support builder infrastructure.')
  JsonSerializable withDefaults() => JsonSerializable(
        anyMap: anyMap ?? defaults.anyMap,
        checked: checked ?? defaults.checked,
        constructor: constructor ?? defaults.constructor,
        createFactory: createFactory ?? defaults.createFactory,
        createToJson: createToJson ?? defaults.createToJson,
        disallowUnrecognizedKeys:
            disallowUnrecognizedKeys ?? defaults.disallowUnrecognizedKeys,
        explicitToJson: explicitToJson ?? defaults.explicitToJson,
        fieldRename: fieldRename ?? defaults.fieldRename,
        ignoreUnannotated: ignoreUnannotated ?? defaults.ignoreUnannotated,
        includeIfNull: includeIfNull ?? defaults.includeIfNull,
        genericArgumentFactories:
            genericArgumentFactories ?? defaults.genericArgumentFactories,
      );

  Map<String, dynamic> toJson() => _$JsonSerializableToJson(this);
}
