// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

// ignore_for_file: avoid_positional_boolean_parameters

import 'package:code_builder/code_builder.dart';
import 'package:smithy_codegen/src/generator/types.dart';

extension ExpressionUtil on Expression {
  /// The property getter, given [isNullable].
  Expression nullableProperty(String name, bool isNullable) {
    if (isNullable) {
      return nullSafeProperty(name);
    } else {
      return property(name);
    }
  }

  Expression wrapWithInlineNullCheck(Expression check) {
    return check.equalTo(literalNull).conditional(literalNull, nullChecked);
  }

  Code wrapWithBlockNullCheck(Expression check, bool isNullable) {
    return isNullable
        ? nullChecked.wrapWithBlockIf(check, isNullable)
        : wrapWithBlockIf(check, isNullable);
  }

  Code wrapWithBlockIf(Expression check, [bool performCheck = true]) {
    return Block.of([
      if (performCheck) ...[
        const Code('if ('),
        check.code,
        const Code(') {'),
      ],
      statement,
      if (performCheck) const Code('}'),
    ]);
  }
}

extension CodeHelpers on Code {
  Code wrapWithBlockIf(Expression check, [bool performCheck = true]) {
    return Block.of([
      if (performCheck) ...[
        const Code('if ('),
        check.code,
        const Code(') {'),
      ],
      this,
      if (performCheck) const Code('}'),
    ]);
  }
}

extension ReferenceHelpers on Reference {
  TypeReference get typeRef =>
      this is TypeReference ? this as TypeReference : type as TypeReference;

  /// Returns a nullable version of `this`.
  TypeReference get boxed {
    return typeRef.rebuild((t) => t.isNullable = true);
  }

  /// Returns a non-nullable version of `this`.
  TypeReference get unboxed {
    return typeRef.rebuild((t) => t.isNullable = false);
  }

  /// Returns a version of `this` with nullable set to [isBoxed].
  TypeReference withBoxed(bool isBoxed) {
    return isBoxed ? boxed : unboxed;
  }

  /// Constructs a `built_value` FullType reference for this.
  Expression fullType([Iterable<Reference>? parameters]) {
    final typeRef = this.typeRef;
    final ctor = typeRef.isNullable ?? false
        ? (Iterable<Expression> args) =>
            DartTypes.builtValue.fullType.constInstanceNamed('nullable', args)
        : DartTypes.builtValue.fullType.constInstance;
    if (typeRef.types.isEmpty && (parameters == null || parameters.isEmpty)) {
      return ctor([typeRef.unboxed]);
    }
    return ctor([
      typeRef.rebuild((t) => t.types.clear()).unboxed,
      literalList(
        parameters?.map((param) => param.fullType()) ??
            typeRef.types.map((t) => t.fullType()),
      ),
    ]);
  }

  /// Whether [symbol] requires transformation in the factory constructor.
  bool get requiresBuiltValueTransformation {
    return url == BuiltValueType.collectionUrl || url == BuiltValueType.jsonUrl;
  }

  /// The `dart:core` symbol for `this`.
  Reference get coreFriendlySymbol {
    final ref = typeRef;
    if (!requiresBuiltValueTransformation) {
      return ref;
    }
    final transformed = switch (ref.symbol) {
      'JsonObject' => DartTypes.core.object,
      'BuiltList' => DartTypes.core.list(
          ref.types.single.coreFriendlySymbol,
        ),
      'BuiltSet' => DartTypes.core.set(
          ref.types.single.coreFriendlySymbol,
        ),
      'BuiltMap' => DartTypes.core.map(
          ref.types[0].coreFriendlySymbol,
          ref.types[1].coreFriendlySymbol,
        ),
      'BuiltListMultimap' => DartTypes.core.map(
          ref.types[0].coreFriendlySymbol,
          DartTypes.core.list(ref.types[1].coreFriendlySymbol),
        ),
      'BuiltSetMultimap' => DartTypes.core.map(
          ref.types[0].coreFriendlySymbol,
          DartTypes.core.set(ref.types[1].coreFriendlySymbol),
        ),
      _ => throw ArgumentError('Invalid symbol: $symbol'),
    };
    return transformed.typeRef.rebuild(
      (t) => t.isNullable = ref.isNullable,
    );
  }

  /// Transforms `built_collection` symbols to their `dart:core` counterpart,
  /// e.g. BuiltList -> List, BuiltSet -> Set, etc for use in factory
  /// constructors so that users do not need to concern themselves with built
  /// types when constructing instances.
  Expression transformToBuiltValue({
    required String name,
    bool? isNullable,
  }) {
    Expression ref = refer(name);
    if (!requiresBuiltValueTransformation) {
      return ref;
    }
    isNullable ??= typeRef.isNullable!;
    final types = typeRef.types;
    switch (typeRef.symbol) {
      case 'BuiltList':
      case 'BuiltSet':
        final childSymbol = types.single;
        if (childSymbol.requiresBuiltValueTransformation) {
          final childExpression = childSymbol.transformToBuiltValue(name: 'el');
          ref = ref.property('map').call([
            Method(
              (m) => m
                ..requiredParameters.add(Parameter((p) => p.name = 'el'))
                ..body = childExpression.code,
            ).closure,
          ]);
        }
      case 'BuiltMap':
        final valueSymbol = types[1];
        if (valueSymbol.requiresBuiltValueTransformation) {
          final valueExpression = valueSymbol.transformToBuiltValue(
            name: 'value',
          );
          ref = ref.property('map').call([
            Method(
              (m) => m
                ..requiredParameters.addAll([
                  Parameter((p) => p.name = 'key'),
                  Parameter((p) => p.name = 'value'),
                ])
                ..body = DartTypes.core.mapEntry
                    .newInstance([refer('key'), valueExpression]).code,
            ).closure,
          ]);
        }
      case 'BuiltListMultimap':
      case 'BuiltSetMultimap':
        final valueSymbol = types[1];
        if (valueSymbol.requiresBuiltValueTransformation) {
          final childExpression = valueSymbol.transformToBuiltValue(
            name: 'el',
          );
          final valueExpression = refer('value').property('map').call([
            Method(
              (m) => m
                ..requiredParameters.add(Parameter((p) => p.name = 'el'))
                ..body = childExpression.code,
            ).closure,
          ]);
          ref = ref.property('map').call([
            Method(
              (m) => m
                ..requiredParameters.addAll([
                  Parameter((p) => p.name = 'key'),
                  Parameter((p) => p.name = 'value'),
                ])
                ..body = DartTypes.core.mapEntry
                    .newInstance([refer('key'), valueExpression]).code,
            ).closure,
          ]);
        }
    }
    ref = refer(symbol!, url).newInstance([ref]);
    return isNullable
        ? refer(name).equalTo(literalNull).conditional(literalNull, ref)
        : ref;
  }
}