// Copyright (c) 2022, 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 'dart:collection';
import 'dart:ffi';
import 'dart:typed_data';

import 'package:collection/collection.dart';
import 'package:ffi/ffi.dart';

import 'jni.dart';
import 'jobject.dart';
import 'jreference.dart';
import 'types.dart';

part 'primitive_jarrays.dart';

final class _$JArray$Type$<E extends JObject?> extends JType<JArray<E>> {
  _$JArray$Type$(JType<E> elementType)
      : signature = '[${elementType.signature}';

  @override
  final String signature;
}

extension type JArray<E extends JObject?>._(JObject _$this) implements JObject {
  /// The type which includes information such as the signature of this class.
  static JType<JArray<$E>> type<$E extends JObject?>(JType<$E> innerType) =>
      _$JArray$Type$<$E>(innerType);

  /// Creates a [JArray] of the given length from the given [elementType].
  ///
  /// The [length] must be a non-negative integer.
  static JArray<$E?> withLength<$E extends JObject?>(
      JType<$E> elementType, int length) {
    RangeError.checkNotNegative(length);
    return _newArray<$E>(elementType.jClass, length);
  }

  static JArray<$E> _newArray<$E extends JObject?>(JClass jClass, int length,
      [$E? fill]) {
    final classRef = jClass.reference;
    final fillRef = fill?.reference ?? jNullReference;
    final array = JObject.fromReference(
      JGlobalReference(Jni.env.NewObjectArray(
        length,
        classRef.pointer,
        fillRef.pointer,
      )),
    ) as JArray<$E>;
    classRef.release();
    return array;
  }

  /// Creates a [JArray] of the given length with [fill] at each position.
  ///
  /// The [length] must be a non-negative integer.
  static JArray<$E> filled<$E extends JObject>(int length, $E fill,
      {JType<$E>? elementType}) {
    RangeError.checkNotNegative(length);
    final jClass = elementType == null ? fill.jClass : elementType.jClass;
    return _newArray<$E>(jClass, length, fill);
  }

  /// Creates a [JArray] from `elements`.
  static JArray<$E> of<$E extends JObject?>(
      JType<$E> elementType, Iterable<$E> elements) {
    final len = elements.length;
    return _newArray<$E>(elementType.jClass, len)..setRange(0, len, elements);
  }

  /// The number of elements in this array.
  int get length => Jni.env.GetArrayLength(reference.pointer);

  E _elementAt(int index) {
    RangeError.checkValueInInterval(index, 0, length - 1);
    final pointer = Jni.env.GetObjectArrayElement(reference.pointer, index);
    if (pointer == nullptr) {
      return null as E;
    }
    return JObject.fromReference(JGlobalReference(pointer)) as E;
  }

  E operator [](int index) {
    return _elementAt(index);
  }

  void operator []=(int index, E value) {
    RangeError.checkValueInInterval(index, 0, length - 1);
    final valueRef = value?.reference ?? jNullReference;
    Jni.env.SetObjectArrayElement(reference.pointer, index, valueRef.pointer);
  }

  void setRange(int start, int end, Iterable<E> iterable, [int skipCount = 0]) {
    RangeError.checkValidRange(start, end, length);
    final rangeLength = end - start;
    final it = iterable.skip(skipCount).take(rangeLength);
    for (final (index, element) in it.indexed) {
      this[index] = element;
    }
  }
}

final class _JArrayListView<E extends JObject?>
    with ListMixin<E>, NonGrowableListMixin<E> {
  final JArray<E> _jarray;

  _JArrayListView(this._jarray);

  @override
  int get length => _jarray.length;

  @override
  E operator [](int index) {
    return _jarray[index];
  }

  @override
  void operator []=(int index, E value) {
    _jarray[index] = value;
  }
}

extension JArrayToList<E extends JObject?> on JArray<E> {
  /// Returns a [List] view into this array.
  ///
  /// Any changes to this list will reflect in the original array as well.
  List<E> asDart() => _JArrayListView<E>(this);
}

void _allocate<T extends NativeType>(
  int byteCount,
  void Function(Pointer<T> ptr) use,
) {
  using((arena) {
    final ptr = arena.allocate<T>(byteCount);
    use(ptr);
  }, malloc);
}

extension on Allocator {
  Pointer<NativeFinalizerFunction>? get _nativeFree {
    return switch (this) {
      malloc => malloc.nativeFree,
      calloc => calloc.nativeFree,
      _ => null,
    };
  }
}
