// Copyright (c) 2023, 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:io';

import 'package:meta/meta.dart' show internal;

import 'third_party/generated_bindings.dart';

// TODO(#567): Add the fact that [JException] is now a [JObject] to the
// CHANGELOG.

mixin _ExplainsRelease on StateError {
  String? get releaseStackTrace;

  @override
  String toString() {
    final sb = StringBuffer(super.toString());
    if (releaseStackTrace != null) {
      sb.write('\n');
      sb.write(releaseStackTrace);
    } else {
      sb.writeln('\nTo see where the object was released, '
          'set `Jni.captureStackTraceOnRelease = true`.');
    }
    return sb.toString();
  }
}

final class UseAfterReleaseError extends StateError with _ExplainsRelease {
  @override
  final String? releaseStackTrace;

  UseAfterReleaseError([this.releaseStackTrace])
      : super('Use after release error');
}

// TODO(#567): Use NullPointerError once it's available.
final class JNullError extends StateError {
  JNullError() : super('The reference was null');
}

final class DoubleReleaseError extends StateError with _ExplainsRelease {
  @override
  final String? releaseStackTrace;

  DoubleReleaseError([this.releaseStackTrace]) : super('Double release error');
}

/// Represents JNI errors that might be returned by methods like `CreateJavaVM`.
sealed class JniError extends Error {
  static const _errors = {
    JniErrorCode.ERR: JniGenericError.new,
    JniErrorCode.EDETACHED: JniThreadDetachedError.new,
    JniErrorCode.EVERSION: JniVersionError.new,
    JniErrorCode.ENOMEM: JniOutOfMemoryError.new,
    JniErrorCode.EEXIST: JniVmExistsError.new,
    JniErrorCode.EINVAL: JniArgumentError.new,
  };

  final String message;

  JniError(this.message);

  @internal
  factory JniError.of(JniErrorCode status) {
    if (!_errors.containsKey(status)) {
      status = JniErrorCode.ERR;
    }
    return _errors[status]!();
  }

  @override
  String toString() {
    return 'JniError: $message';
  }
}

final class JniGenericError extends JniError {
  JniGenericError() : super('Generic JNI error');
}

final class JniThreadDetachedError extends JniError {
  JniThreadDetachedError() : super('Thread detached from VM');
}

final class JniVersionError extends JniError {
  JniVersionError() : super('JNI version error');
}

final class JniOutOfMemoryError extends JniError {
  JniOutOfMemoryError() : super('Out of memory');
}

final class JniVmExistsError extends JniError {
  JniVmExistsError() : super('VM Already created');
}

final class JniArgumentError extends JniError {
  JniArgumentError() : super('Invalid arguments');
}

final class NoJvmInstanceError extends Error {
  @override
  String toString() => 'No JNI instance is available';
}

final class HelperNotFoundError extends Error {
  final String path;

  HelperNotFoundError(this.path);

  @override
  String toString() => '''
Unable to locate the helper library.

Ensure that the helper library is available at the path: $path
Run `dart jni:setup` to generate the shared library if it does not exist.

Note: If the --build-path option is passed to jni:setup, Jni.spawn must be
called with same dylibDir. Also when creating new Dart isolates, Jni.setDylibDir
must be called.
''';
}

final class DynamicLibraryLoadError extends Error {
  final String libraryPath;

  DynamicLibraryLoadError(this.libraryPath);

  @override
  String toString() {
    return '''
Failed to load dynamic library at path: $libraryPath
The library was found at the specified path, but it could not be loaded. 
This might be due to missing dependencies or incorrect file permissions. 
Please ensure ${Platform.isWindows ? r'that `\bin\server\jvm.dll` is in the PATH, and ' : ''}that the file has the correct permissions.

''';
  }
}

final class JniNewStringException implements Exception {
  final String string;
  JniNewStringException(this.string);

  @override
  String toString() => 'Failed to convert string to a JString: $string';
}
