// 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.

import 'dart:async';
import 'dart:convert';
import 'dart:typed_data';

import 'package:meta/meta.dart';

import '../http.dart' as http;
import 'base_client.dart';
import 'base_request.dart';
import 'client_stub.dart'
    if (dart.library.js_interop) 'browser_client.dart'
    if (dart.library.io) 'io_client.dart';
import 'exception.dart';
import 'response.dart';
import 'streamed_response.dart';

/// The interface for HTTP clients that take care of maintaining persistent
/// connections across multiple requests to the same server.
///
/// If you only need to send a single request, it's usually easier to use
/// [http.head], [http.get], [http.post], [http.put], [http.patch], or
/// [http.delete] instead.
///
/// All methods will emit a [ClientException] if there is a transport-level
/// failure when communication with the server. For example, if the server could
/// not be reached.
///
/// When creating an HTTP client class with additional functionality, you must
/// extend [BaseClient] rather than [Client]. In most cases, you can wrap
/// another instance of [Client] and add functionality on top of that. This
/// allows all classes implementing [Client] to be mutually composable.
abstract interface class Client {
  /// Creates a new platform appropriate client.
  ///
  /// Creates an `IOClient` if `dart:io` is available and a `BrowserClient` if
  /// `dart:js_interop` is available, otherwise it will throw an unsupported
  /// error.
  factory Client() => zoneClient ?? createClient();

  /// Sends an HTTP HEAD request with the given headers to the given URL.
  ///
  /// For more fine-grained control over the request, use [send] instead.
  Future<Response> head(Uri url, {Map<String, String>? headers});

  /// Sends an HTTP GET request with the given headers to the given URL.
  ///
  /// For more fine-grained control over the request, use [send] instead.
  Future<Response> get(Uri url, {Map<String, String>? headers});

  /// Sends an HTTP POST request with the given headers and body to the given
  /// URL.
  ///
  /// [body] sets the body of the request. It can be a `String`, a `List<int>`
  /// or a `Map<String, String>`.
  ///
  /// If [body] is a `String`, it's encoded using [encoding] and used as the
  /// body of the request. The content-type of the request will default to
  /// "text/plain".
  ///
  /// If [body] is a `List`, it's used as a list of bytes for the body of the
  /// request.
  ///
  /// If [body] is a `Map`, it's encoded as form fields using [encoding]. The
  /// content-type of the request will be set to
  /// `"application/x-www-form-urlencoded"`; this cannot be overridden.
  ///
  /// [encoding] defaults to [utf8].
  ///
  /// For more fine-grained control over the request, use [send] instead.
  Future<Response> post(Uri url,
      {Map<String, String>? headers, Object? body, Encoding? encoding});

  /// Sends an HTTP PUT request with the given headers and body to the given
  /// URL.
  ///
  /// [body] sets the body of the request. It can be a `String`, a `List<int>`
  /// or a `Map<String, String>`. If it's a `String`, it's encoded using
  /// [encoding] and used as the body of the request. The content-type of the
  /// request will default to "text/plain".
  ///
  /// If [body] is a `List`, it's used as a list of bytes for the body of the
  /// request.
  ///
  /// If [body] is a `Map`, it's encoded as form fields using [encoding]. The
  /// content-type of the request will be set to
  /// `"application/x-www-form-urlencoded"`; this cannot be overridden.
  ///
  /// [encoding] defaults to [utf8].
  ///
  /// For more fine-grained control over the request, use [send] instead.
  Future<Response> put(Uri url,
      {Map<String, String>? headers, Object? body, Encoding? encoding});

  /// Sends an HTTP PATCH request with the given headers and body to the given
  /// URL.
  ///
  /// [body] sets the body of the request. It can be a `String`, a `List<int>`
  /// or a `Map<String, String>`. If it's a `String`, it's encoded using
  /// [encoding] and used as the body of the request. The content-type of the
  /// request will default to "text/plain".
  ///
  /// If [body] is a `List`, it's used as a list of bytes for the body of the
  /// request.
  ///
  /// If [body] is a `Map`, it's encoded as form fields using [encoding]. The
  /// content-type of the request will be set to
  /// `"application/x-www-form-urlencoded"`; this cannot be overridden.
  ///
  /// [encoding] defaults to [utf8].
  ///
  /// For more fine-grained control over the request, use [send] instead.
  Future<Response> patch(Uri url,
      {Map<String, String>? headers, Object? body, Encoding? encoding});

  /// Sends an HTTP DELETE request with the given headers to the given URL.
  ///
  /// For more fine-grained control over the request, use [send] instead.
  Future<Response> delete(Uri url,
      {Map<String, String>? headers, Object? body, Encoding? encoding});

  /// Sends an HTTP GET request with the given headers to the given URL and
  /// returns a Future that completes to the body of the response as a String.
  ///
  /// The Future will emit a [ClientException] if the response doesn't have a
  /// success status code.
  ///
  /// For more fine-grained control over the request and response, use [send] or
  /// [get] instead.
  Future<String> read(Uri url, {Map<String, String>? headers});

  /// Sends an HTTP GET request with the given headers to the given URL and
  /// returns a Future that completes to the body of the response as a list of
  /// bytes.
  ///
  /// The Future will emit a [ClientException] if the response doesn't have a
  /// success status code.
  ///
  /// For more fine-grained control over the request and response, use [send] or
  /// [get] instead.
  Future<Uint8List> readBytes(Uri url, {Map<String, String>? headers});

  /// Sends an HTTP request and asynchronously returns the response.
  Future<StreamedResponse> send(BaseRequest request);

  /// Closes the client and cleans up any resources associated with it.
  ///
  /// Some clients maintain a pool of network connections that will not be
  /// disconnected until the client is closed. This may cause programs using
  /// using the Dart SDK (`dart run`, `dart test`, `dart compile`, etc.) to
  /// not terminate until the client is closed. Programs run using the Flutter
  /// SDK can still terminate even with an active connection pool.
  ///
  /// Once [close] is called, no other methods should be called. If [close] is
  /// called while other asynchronous methods are running, the behavior is
  /// undefined.
  void close();
}

/// The [Client] for the current [Zone], if one has been set.
///
/// NOTE: This property is explicitly hidden from the public API.
@internal
Client? get zoneClient {
  final client = Zone.current[#_clientToken];
  return client == null ? null : (client as Client Function())();
}

/// Runs [body] in its own [Zone] with the [Client] returned by [clientFactory]
/// set as the default [Client].
///
/// For example:
///
/// ```
/// class MyAndroidHttpClient extends BaseClient {
///   @override
///   Future<http.StreamedResponse> send(http.BaseRequest request) {
///     // your implementation here
///   }
/// }
///
/// void main() {
///  var clientFactory = Client.new; // Constructs the default client.
///  if (Platform.isAndroid) {
///     clientFactory = MyAndroidHttpClient.new;
///  }
///  runWithClient(myFunction, clientFactory);
/// }
///
/// void myFunction() {
///   // Uses the `Client` configured in `main`.
///   final response = await get(Uri.https('www.example.com', ''));
///   final client = Client();
/// }
/// ```
///
/// The [Client] returned by [clientFactory] is used by the [Client.new] factory
/// and the convenience HTTP functions (e.g. [http.get]). If [clientFactory]
/// returns `Client()` then the default [Client] is used.
///
/// Only fresh `Client` instances using the default constructor are impacted.
/// HTTP requests made using `dart:io` or `dart:html` APIs,
/// or using specifically instantiated client implementations, are not affected.
///
/// If [runWithClient] is used and the environment defines
/// `no_default_http_client=true` then generated binaries may be smaller e.g.
/// ```shell
/// $ dart compile exe --define=no_default_http_client=true ...
/// ```
///
/// If `no_default_http_client=true` is set then any call to the [Client]
/// factory (i.e. `Client()`) outside of the [Zone] created by [runWithClient]
/// will throw [StateError].
///
/// > [!IMPORTANT]
/// > Flutter does not guarantee that callbacks are executed in a particular
/// > [Zone].
/// >
/// > Instead of using [runWithClient], Flutter developers can use a framework,
/// > such as [`package:provider`](https://pub.dev/packages/provider), to make
/// > a [Client] available throughout their applications.
/// >
/// > See the
/// > [Flutter Http Example](https://github.com/dart-lang/http/tree/master/pkgs/flutter_http_example).
R runWithClient<R>(R Function() body, Client Function() clientFactory,
        {ZoneSpecification? zoneSpecification}) =>
    runZoned(body,
        zoneValues: {#_clientToken: Zone.current.bindCallback(clientFactory)},
        zoneSpecification: zoneSpecification);
