// 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:collection';

import 'package:meta/meta.dart';

import '../http.dart' show ClientException, get;
import 'abortable.dart';
import 'base_client.dart';
import 'base_response.dart';
import 'byte_stream.dart';
import 'client.dart';
import 'streamed_response.dart';
import 'utils.dart';

/// The base class for HTTP requests.
///
/// Subclasses of [BaseRequest] can be constructed manually and passed to
/// [BaseClient.send], which allows the user to provide fine-grained control
/// over the request properties. However, usually it's easier to use convenience
/// methods like [get] or [BaseClient.get].
///
/// Subclasses/implementers should mixin/implement [Abortable] to support
/// request cancellation. A future breaking version of 'package:http' will
/// merge [Abortable] into [BaseRequest], making it a requirement.
abstract class BaseRequest {
  /// The HTTP method of the request.
  ///
  /// Most commonly "GET" or "POST", less commonly "HEAD", "PUT", or "DELETE".
  /// Non-standard method names are also supported.
  final String method;

  /// The URL to which the request will be sent.
  final Uri url;

  /// The size of the request body, in bytes.
  ///
  /// This defaults to `null`, which indicates that the size of the request is
  /// not known in advance. May not be assigned a negative value.
  int? get contentLength => _contentLength;
  int? _contentLength;

  set contentLength(int? value) {
    if (value != null && value < 0) {
      throw ArgumentError('Invalid content length $value.');
    }
    _checkFinalized();
    _contentLength = value;
  }

  /// Whether a persistent connection should be maintained with the server.
  ///
  /// Defaults to true.
  bool get persistentConnection => _persistentConnection;
  bool _persistentConnection = true;

  set persistentConnection(bool value) {
    _checkFinalized();
    _persistentConnection = value;
  }

  /// Whether the client should follow redirects while resolving this request.
  ///
  /// Defaults to true.
  bool get followRedirects => _followRedirects;
  bool _followRedirects = true;

  set followRedirects(bool value) {
    _checkFinalized();
    _followRedirects = value;
  }

  /// The maximum number of redirects to follow when [followRedirects] is true.
  ///
  /// If this number is exceeded the [BaseResponse] future will signal a
  /// [ClientException]. Defaults to 5.
  int get maxRedirects => _maxRedirects;
  int _maxRedirects = 5;

  set maxRedirects(int value) {
    _checkFinalized();
    _maxRedirects = value;
  }

  // TODO(nweiz): automatically parse cookies from headers

  /// The HTTP headers sent to the server.
  ///
  /// Header names are converted to lowercase when sent to the server.
  ///
  /// Some headers may not be sent by the [Client] for security or privacy
  /// reasons. For example, browser-based clients may only sent headers in the
  /// CORS safelist or specifically allowed by the server.
  final Map<String, String> headers;

  /// Whether [finalize] has been called.
  bool get finalized => _finalized;
  bool _finalized = false;

  static final _tokenRE = RegExp(r"^[\w!#%&'*+\-.^`|~]+$");
  static String _validateMethod(String method) {
    if (!_tokenRE.hasMatch(method)) {
      throw ArgumentError.value(method, 'method', 'Not a valid method');
    }
    return method;
  }

  BaseRequest(String method, this.url)
      : method = _validateMethod(method),
        headers = LinkedHashMap(
            equals: (key1, key2) => key1.toLowerCase() == key2.toLowerCase(),
            hashCode: (key) => key.toLowerCase().hashCode);

  /// Finalizes the HTTP request in preparation for it being sent.
  ///
  /// Freezes all mutable fields and returns a single-subscription [ByteStream]
  /// that emits the body of the request.
  ///
  /// The base implementation of this returns an empty [ByteStream];
  /// subclasses are responsible for creating the return value, which should be
  /// single-subscription to ensure that no data is dropped. They should also
  /// freeze any additional mutable fields they add that don't make sense to
  /// change after the request headers are sent.
  @mustCallSuper
  ByteStream finalize() {
    // TODO(nweiz): freeze headers
    if (finalized) throw StateError("Can't finalize a finalized Request.");
    _finalized = true;
    return const ByteStream(Stream.empty());
  }

  /// Sends this request.
  ///
  /// This automatically initializes a new [Client] and closes that client once
  /// the request is complete. If you're planning on making multiple requests to
  /// the same server, you should use a single [Client] for all of those
  /// requests.
  Future<StreamedResponse> send() async {
    var client = Client();

    try {
      var response = await client.send(this);
      var stream = onDone(response.stream, client.close);

      if (response case BaseResponseWithUrl(:final url)) {
        return StreamedResponseV2(ByteStream(stream), response.statusCode,
            contentLength: response.contentLength,
            request: response.request,
            headers: response.headers,
            isRedirect: response.isRedirect,
            url: url,
            persistentConnection: response.persistentConnection,
            reasonPhrase: response.reasonPhrase);
      } else {
        return StreamedResponse(ByteStream(stream), response.statusCode,
            contentLength: response.contentLength,
            request: response.request,
            headers: response.headers,
            isRedirect: response.isRedirect,
            persistentConnection: response.persistentConnection,
            reasonPhrase: response.reasonPhrase);
      }
    } catch (_) {
      client.close();
      rethrow;
    }
  }

  /// Throws an error if this request has been finalized.
  void _checkFinalized() {
    if (!finalized) return;
    throw StateError("Can't modify a finalized Request.");
  }

  @override
  String toString() => '$method $url';
}
