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

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

void main() async {
  test('sync', () async {
    final freed = <int>[];
    void freeInt(int i) {
      freed.add(i);
    }

    using((Arena arena) {
      arena.using(1234, freeInt);
      expect(freed.isEmpty, true);
    });
    expect(freed, [1234]);
  });

  test('async', () async {
    /// Calling [using] waits with releasing its resources until after
    /// [Future]s complete.
    final freed = <int>[];
    void freeInt(int i) {
      freed.add(i);
    }

    final Future<int> myFutureInt = using((Arena arena) {
      return Future.microtask(() {
        arena.using(1234, freeInt);
        return 1;
      });
    });

    expect(freed.isEmpty, true);
    await myFutureInt;
    expect(freed, [1234]);
  });

  test('throw', () {
    /// [using] waits with releasing its resources until after [Future]s
    /// complete.
    final freed = <int>[];
    void freeInt(int i) {
      freed.add(i);
    }

    // Resources are freed also when abnormal control flow occurs.
    var didThrow = false;
    try {
      using((Arena arena) {
        arena.using(1234, freeInt);
        expect(freed.isEmpty, true);
        throw Exception('Exception 1');
      });
    } on Exception {
      expect(freed.single, 1234);
      didThrow = true;
    }
    expect(didThrow, true);
  });

  test('allocate', () {
    final countingAllocator = CountingAllocator();
    // To ensure resources are freed, wrap them in a [using] call.
    using((Arena arena) {
      final p = arena<Int64>(2);
      p[1] = p[0];
    }, countingAllocator);
    expect(countingAllocator.freeCount, 1);
  });

  test('allocate throw', () {
    // Resources are freed also when abnormal control flow occurs.
    var didThrow = false;
    final countingAllocator = CountingAllocator();
    try {
      using((Arena arena) {
        final p = arena<Int64>(2);
        p[0] = 25;
        throw Exception('Exception 2');
      }, countingAllocator);
    } on Exception {
      expect(countingAllocator.freeCount, 1);
      didThrow = true;
    }
    expect(didThrow, true);
  });

  test('toNativeUtf8', () {
    final countingAllocator = CountingAllocator();
    using((Arena arena) {
      final p = 'Hello world!'.toNativeUtf8(allocator: arena);
      expect(p.toDartString(), 'Hello world!');
    }, countingAllocator);
    expect(countingAllocator.freeCount, 1);
  });

  test('zone', () async {
    final freed = <int>[];
    void freeInt(int i) {
      freed.add(i);
    }

    withZoneArena(() {
      zoneArena.using(1234, freeInt);
      expect(freed.isEmpty, true);
    });
    expect(freed.length, 1);
    expect(freed.single, 1234);
  });

  test('zone async', () async {
    /// [using] waits with releasing its resources until after [Future]s
    /// complete.
    final freed = <int>[];
    void freeInt(int i) {
      freed.add(i);
    }

    final Future<int> myFutureInt = withZoneArena(() {
      return Future.microtask(() {
        zoneArena.using(1234, freeInt);
        return 1;
      });
    });

    expect(freed.isEmpty, true);
    await myFutureInt;
    expect(freed.length, 1);
    expect(freed.single, 1234);
  });

  test('zone throw', () {
    /// [using] waits with releasing its resources until after [Future]s
    /// complete.
    final freed = <int>[];
    void freeInt(int i) {
      freed.add(i);
    }

    // Resources are freed also when abnormal control flow occurs.
    var didThrow = false;
    try {
      withZoneArena(() {
        zoneArena.using(1234, freeInt);
        expect(freed.isEmpty, true);
        throw Exception('Exception 3');
      });
    } on Exception {
      expect(freed.single, 1234);
      didThrow = true;
    }
    expect(didThrow, true);
    expect(freed.single, 1234);
  });

  test('zone future error', () async {
    var caughtError = false;
    var uncaughtError = false;

    Future<int> asyncFunction() async {
      throw Exception('Exception 4');
    }

    final future = runZonedGuarded(
      () {
        return withZoneArena(asyncFunction).catchError((error) {
          caughtError = true;
          return 5;
        });
      },
      (error, stackTrace) {
        uncaughtError = true;
      },
    );

    final result = (await Future.wait([future!])).single;

    expect(result, 5);
    expect(caughtError, true);
    expect(uncaughtError, false);
  });

  test('allocate during releaseAll', () {
    final countingAllocator = CountingAllocator();
    final arena = Arena(countingAllocator);

    arena.using(arena<Uint8>(), (Pointer discard) {
      arena<Uint8>();
    });

    expect(countingAllocator.allocationCount, 1);
    expect(countingAllocator.freeCount, 0);

    arena.releaseAll(reuse: true);

    expect(countingAllocator.allocationCount, 2);
    expect(countingAllocator.freeCount, 2);
  });
}

/// Keeps track of the number of allocates and frees for testing purposes.
class CountingAllocator implements Allocator {
  final Allocator wrappedAllocator;

  int allocationCount = 0;
  int freeCount = 0;

  CountingAllocator([this.wrappedAllocator = calloc]);

  @override
  Pointer<T> allocate<T extends NativeType>(int byteCount, {int? alignment}) {
    allocationCount++;
    return wrappedAllocator.allocate(byteCount, alignment: alignment);
  }

  @override
  void free(Pointer<NativeType> pointer) {
    freeCount++;
    return wrappedAllocator.free(pointer);
  }
}
