// Copyright (c) 2012, 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.

part of 'internal.dart';

/// An object representing a protobuf message field.
class FieldInfo<T> {
  /// Cached read-only empty list for this field type. For non-repeated fields
  /// this is always `null`. Otherwise it starts as `null` and gets initialized
  /// in `readonlyDefault`.
  PbList<T>? _emptyList;

  /// Name of this field as the `json_name` reported by protoc.
  ///
  /// Example:
  ///
  /// ```proto
  /// message Msg {
  ///   int32 foo_name = 1 [json_name = "barName"];
  /// }
  /// ```
  ///
  /// Here `name` of the field is `barName`. When `json_name` is not specified
  /// in the proto definition, this is the camelCase version of the field name.
  /// In the example above, without the `json_name` field option, `name` would
  /// be `"fooName"`.
  final String name;

  /// Name of this field as written in the proto definition.
  ///
  /// Example:
  ///
  /// ```proto
  /// message SearchRequest {
  ///   ...
  ///   int32 result_per_page = 3;
  /// }
  /// ```
  ///
  /// `protoName` for the `result_per_page` field above is `"result_per_page"`.
  /// The name typically consist of words separated with underscores.
  String get protoName {
    return _protoName ??= _unCamelCase(name);
  }

  String? _protoName;

  /// Field number as specified in the proto definition.
  ///
  /// Example:
  ///
  /// ```proto
  /// message SearchRequest {
  ///   ...
  ///   int32 result_per_page = 3;
  /// }
  /// ```
  ///
  /// `tagNumber` of `result_per_page` field is 3.
  final int tagNumber;

  /// Index of the field in [FieldSet._values] list of this field's message.
  ///
  /// The value is `null` for extension fields.
  final int? index;

  /// Type of this field. See [PbFieldType].
  final int type;

  /// Constructs the default value of a field.
  ///
  /// For repeated fields, only used when the [check] property is `null`.
  final MakeDefaultFunc? makeDefault;

  /// Creates an empty message or group when decoding a message.
  ///
  /// Only available in fields with message type.
  final CreateBuilderFunc? subBuilder;

  /// List of all enum values.
  ///
  /// Only available in enum fields.
  final List<ProtobufEnum>? enumValues;

  /// Default enum value.
  ///
  /// Only available in enum fields.
  final ProtobufEnum? defaultEnumValue;

  /// Mapping from enum integer values to enum values.
  ///
  /// Only available in enum fields.
  final ValueOfFunc? valueOf;

  /// Function to verify items when adding to a repeated field.
  ///
  /// Only available in repeated fields.
  final CheckFunc<T>? check;

  FieldInfo(
    this.name,
    this.tagNumber,
    this.index,
    this.type, {
    dynamic defaultOrMaker,
    this.subBuilder,
    this.valueOf,
    this.enumValues,
    this.defaultEnumValue,
    String? protoName,
  }) : makeDefault = findMakeDefault(type, defaultOrMaker),
       check = null,
       _protoName = protoName,
       assert(type != 0),
       assert(
         !PbFieldType.isGroupOrMessage(type) ||
             subBuilder != null ||
             PbFieldType.isMapField(type),
       ),
       assert(!PbFieldType.isEnum(type) || valueOf != null);

  // Represents a field that has been removed by a program transformation.
  FieldInfo.dummy(this.index)
    : name = '<removed field>',
      _protoName = '<removed field>',
      tagNumber = 0,
      type = 0,
      makeDefault = null,
      valueOf = null,
      check = null,
      enumValues = null,
      defaultEnumValue = null,
      subBuilder = null;

  FieldInfo.repeated(
    this.name,
    this.tagNumber,
    this.index,
    this.type,
    this.check,
    this.subBuilder, {
    this.valueOf,
    this.enumValues,
    this.defaultEnumValue,
    String? protoName,
  }) : makeDefault = (() => newPbList<T>(check: check)),
       _protoName = protoName,
       assert(PbFieldType.isRepeated(type)),
       assert(!PbFieldType.isEnum(type) || valueOf != null);

  static MakeDefaultFunc? findMakeDefault(int type, dynamic defaultOrMaker) {
    if (defaultOrMaker == null) return PbFieldType.defaultForType(type);
    if (defaultOrMaker is MakeDefaultFunc) return defaultOrMaker;
    return () => defaultOrMaker;
  }

  /// Whether this represents a dummy field standing in for a field that has
  /// been removed by a program transformation.
  bool get _isDummy => tagNumber == 0;

  bool get isRequired => PbFieldType.isRequired(type);
  bool get isRepeated => PbFieldType.isRepeated(type);
  bool get isGroupOrMessage => PbFieldType.isGroupOrMessage(type);
  bool get isEnum => PbFieldType.isEnum(type);
  bool get isMapField => PbFieldType.isMapField(type);

  /// Returns a read-only default value for a field. Unlike
  /// [GeneratedMessage.getField], doesn't create a repeated field.
  dynamic get readonlyDefault {
    if (isRepeated) {
      return _emptyList ??= newUnmodifiablePbList();
    }
    return makeDefault!();
  }

  /// Returns true if the field's value is okay to transmit.
  /// That is, it doesn't contain any required fields that aren't initialized.
  bool _hasRequiredValues(dynamic value) {
    if (value == null) return !isRequired; // missing is okay if optional
    if (!PbFieldType.isGroupOrMessage(type)) {
      return true; // primitive and present
    }

    if (!isRepeated) {
      // A required message: recurse.
      final GeneratedMessage message = value;
      return message._fieldSet._hasRequiredValues();
    }

    final List<GeneratedMessage> list = value;
    if (list.isEmpty) return true;

    // For message types that (recursively) contain no required fields,
    // short-circuit the loop.
    if (!list[0]._fieldSet._hasRequiredFields) return true;

    // Recurse on each item in the list.
    return list.every((GeneratedMessage m) => m._fieldSet._hasRequiredValues());
  }

  /// Appends the dotted path to each required field that's missing a value.
  void _appendInvalidFields(List<String> problems, value, String prefix) {
    if (value == null) {
      if (isRequired) problems.add('$prefix$name');
    } else if (!PbFieldType.isGroupOrMessage(type)) {
      // primitive and present
    } else if (!isRepeated) {
      // Required message/group: recurse.
      final GeneratedMessage message = value;
      message._fieldSet._appendInvalidFields(problems, '$prefix$name.');
    } else {
      final List<GeneratedMessage> list = value;
      if (list.isEmpty) return;

      // For message types that (recursively) contain no required fields,
      // short-circuit the loop.
      if (!list[0]._fieldSet._hasRequiredFields) return;

      // Recurse on each item in the list.
      var position = 0;
      for (final message in list) {
        message._fieldSet._appendInvalidFields(
          problems,
          '$prefix$name[$position].',
        );
        position++;
      }
    }
  }

  /// Creates a repeated field.
  PbList<T> _createRepeatedField() {
    assert(isRepeated);
    return newPbList<T>(check: check);
  }

  /// Same as above, but allow a tighter typed [PbList] to be created.
  PbList<S> _createRepeatedFieldWithType<S extends T>() {
    assert(isRepeated);
    return newPbList<S>(check: check);
  }

  /// Convenience method to thread this FieldInfo's reified type parameter to
  /// `FieldSet._ensureRepeatedField`.
  PbList<T> _ensureRepeatedField(BuilderInfo meta, FieldSet fs) {
    return fs._ensureRepeatedField<T>(meta, this);
  }

  @override
  String toString() => name;
}

extension FieldInfoInternalExtension<T> on FieldInfo<T> {
  List<T> ensureRepeatedField(BuilderInfo meta, FieldSet fs) =>
      _ensureRepeatedField(meta, fs);
}

final RegExp _upperCase = RegExp('[A-Z]');

String _unCamelCase(String name) {
  return name.replaceAllMapped(
    _upperCase,
    (match) => '_${match.group(0)!.toLowerCase()}',
  );
}

/// A [FieldInfo] subclass for protobuf `map` fields.
class MapFieldInfo<K, V> extends FieldInfo<PbMap<K, V>?> {
  /// Key type of the map. Per proto2 and proto3 specs, this needs to be an
  /// integer type or `string`, and the type cannot be `repeated`.
  ///
  /// The `int` value is interpreted the same way as [FieldInfo.type].
  final int keyFieldType;

  /// Value type of the map. Per proto2 and proto3 specs, this can be any type
  /// other than `map`, and the type cannot be `repeated`.
  ///
  /// The `int` value is interpreted the same way as [FieldInfo.type].
  final int valueFieldType;

  /// Creates a new empty instance of the value type.
  ///
  /// `null` if the value type is not a Message type.
  final CreateBuilderFunc? valueCreator;

  final BuilderInfo mapEntryBuilderInfo;

  MapFieldInfo(
    String name,
    int tagNumber,
    int index,
    int type,
    this.keyFieldType,
    this.valueFieldType,
    this.mapEntryBuilderInfo,
    this.valueCreator, {
    ProtobufEnum? defaultEnumValue,
    String? protoName,
  }) : assert(PbFieldType.isMapField(type)),
       super(
         name,
         tagNumber,
         index,
         type,
         defaultOrMaker: () => newPbMap<K, V>(keyFieldType, valueFieldType),
         defaultEnumValue: defaultEnumValue,
         protoName: protoName,
       ) {
    assert(!PbFieldType.isEnum(type) || valueOf != null);
  }

  FieldInfo get valueFieldInfo =>
      mapEntryBuilderInfo.fieldInfo[mapValueFieldNumber]!;

  PbMap<K, V> _ensureMapField(BuilderInfo meta, FieldSet fs) {
    return fs._ensureMapField<K, V>(meta, this);
  }

  PbMap<K, V> _createMapField() {
    assert(isMapField);
    return newPbMap<K, V>(keyFieldType, valueFieldType);
  }
}

extension MapFieldInfoInternalExtension<K, V> on MapFieldInfo<K, V> {
  Map<K, V> ensureMapField(BuilderInfo meta, FieldSet fs) =>
      _ensureMapField(meta, fs);
}
