// 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:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:mojo/core.dart' as mojo;
import 'package:mojo/mojo/http_header.mojom.dart' as mojom;
import 'package:mojo/mojo/url_request.mojom.dart' as mojom;
import 'package:mojo/mojo/url_response.mojom.dart' as mojom;
import 'package:mojo_services/mojo/network_service.mojom.dart' as mojom;
import 'package:mojo_services/mojo/url_loader.mojom.dart' as mojom;

import 'response.dart';

/// A `mojo`-based HTTP client.
class MojoClient {

  /// Sends an HTTP HEAD request with the given headers to the given URL, which
  /// can be a [Uri] or a [String].
  ///
  /// Network errors will be turned into [Response] object with a non-null
  /// [Response.error] field.
  Future<Response> head(dynamic url, { Map<String, String> headers }) {
    return _createResponse(_send("HEAD", url, headers));
  }

  /// Sends an HTTP GET request with the given headers to the given URL, which can
  /// be a [Uri] or a [String].
  ///
  /// Network errors will be turned into [Response] object with a non-null
  /// [Response.error] field.
  Future<Response> get(dynamic url, { Map<String, String> headers }) {
    return _createResponse(_send("GET", url, headers));
  }

  /// Sends an HTTP POST request with the given headers and body to the given URL,
  /// which can be a [Uri] or a [String].
  ///
  /// [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].
  ///
  /// Network errors will be turned into [Response] object with a non-null
  /// [Response.error] field.
  Future<Response> post(dynamic url, { Map<String, String> headers, dynamic body, Encoding encoding: UTF8 }) {
    return _createResponse(_send("POST", url, headers, body, encoding));
  }

  /// Sends an HTTP PUT request with the given headers and body to the given URL,
  /// which can be a [Uri] or a [String].
  ///
  /// [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].
  ///
  /// Network errors will be turned into [Response] object with a non-null
  /// [Response.error] field.
  Future<Response> put(dynamic url, { Map<String, String> headers, dynamic body, Encoding encoding: UTF8 }) {
    return _createResponse(_send("PUT", url, headers, body, encoding));
  }

  /// Sends an HTTP PATCH request with the given headers and body to the given
  /// URL, which can be a [Uri] or a [String].
  ///
  /// [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].
  ///
  /// Network errors will be turned into [Response] object with a non-null
  /// [Response.error] field.
  Future<Response> patch(dynamic url, {Map<String, String> headers, dynamic body, Encoding encoding: UTF8 }) {
    return _createResponse(_send("PATCH", url, headers, body, encoding));
  }

  /// Sends an HTTP DELETE request with the given headers to the given URL, which
  /// can be a [Uri] or a [String].
  ///
  /// Network errors will be turned into [Response] object with a non-null
  /// [Response.error] field.
  Future<Response> delete(dynamic url, { Map<String, String> headers }) {
    return _createResponse(_send("DELETE", url, headers));
  }

  /// Sends an HTTP GET request with the given headers to the given URL, which can
  /// be a [Uri] or a [String], and returns a Future that completes to the body of
  /// the response as a [String].
  ///
  /// The Future will resolve with an error in the case of a network error or if
  /// the response doesn't have a success status code.
  Future<String> read(dynamic url, { Map<String, String> headers }) {
    return get(url, headers: headers).then((Response response) {
      _requireSuccess(url, response.statusCode, response.error);
      return response.body;
    });
  }

  /// Sends an HTTP GET request with the given headers to the given URL, which can
  /// be a [Uri] or a [String], and returns a Future that completes to the body of
  /// the response as a list of bytes.
  ///
  /// The Future will resolve with an error in the case of a network error or if
  /// the response doesn't have a success status code.
  Future<Uint8List> readBytes(dynamic url, { Map<String, String> headers }) {
    return get(url, headers: headers).then((Response response) {
      _requireSuccess(url, response.statusCode, response.error);
      return response.bodyBytes;
    });
  }

  /// Sends an HTTP GET request with the given headers to the given URL, which can
  /// be a [Uri] or a [String], and returns a Future that completes to the body of
  /// the response as a [mojo.MojoDataPipeConsumer].
  ///
  /// The Future will resolve with an error in the case of a network error or if
  /// the response doesn't have a success status code.
  Future<mojo.MojoDataPipeConsumer> readDataPipe(dynamic url, { Map<String, String> headers }) {
    return _send('GET', url, headers).then((mojom.UrlResponse response) {
      _requireSuccess(url, response.statusCode, response.statusLine);
      return response.body;
    });
  }

  mojom.UrlRequest _prepareRequest(String method, dynamic url, Map<String, String> headers, [dynamic body, Encoding encoding = UTF8]) {
    List<mojom.HttpHeader> mojoHeaders = <mojom.HttpHeader>[];
    headers?.forEach((String name, String value) {
      mojom.HttpHeader header = new mojom.HttpHeader()
        ..name = name
        ..value = value;
      mojoHeaders.add(header);
    });
    mojom.UrlRequest request = new mojom.UrlRequest()
      ..url = url.toString()
      ..headers = mojoHeaders
      ..method = method
      ..autoFollowRedirects = true;
    if (body != null) {
      mojo.MojoDataPipe pipe = new mojo.MojoDataPipe();
      request.body = <mojo.MojoDataPipeConsumer>[pipe.consumer];
      Uint8List encodedBody = encoding.encode(body);
      ByteData data = new ByteData.view(encodedBody.buffer);
      mojo.DataPipeFiller.fillHandle(pipe.producer, data);
    }
    return request;
  }

  Future<mojom.UrlResponse> _send(String method, dynamic url, Map<String, String> headers, [dynamic body, Encoding encoding = UTF8]) {
    Completer<mojom.UrlResponse> completer = new Completer<mojom.UrlResponse>();
    mojom.UrlLoaderProxy loader = new mojom.UrlLoaderProxy.unbound();
    networkService.createUrlLoader(loader);
    mojom.UrlRequest request = _prepareRequest(method, url, headers, body, encoding);
    loader.start(request, (mojom.UrlResponse response) async {
      loader.close();
      try {
        if (response.error != null)
          throw new Exception('Request to "$url" failed with error ${response.error.code}.\n${response.error.description}');
        if (!response.body.handle.isValid)
          throw new Exception('Response body does not have a valid handle, but no error was reported.\n${response.body}');
        completer.complete(response);
      } catch (e, stack) {
        FlutterError.reportError(new FlutterErrorDetails(
          exception: e,
          stack: stack,
          library: 'networking HTTP library',
          context: 'while interacting with the Mojo network library',
          silent: true
        ));
        completer.completeError(e);
      }
    });
    return completer.future;
  }

  Future<Response> _createResponse(Future<mojom.UrlResponse> futureResponse) async {
    try {
      mojom.UrlResponse response = await futureResponse;
      try {
        ByteData data = await mojo.DataPipeDrainer.drainHandle(response.body);
        Uint8List bodyBytes = new Uint8List.view(data.buffer);
        Map<String, String> headers = <String, String>{};
        if (response.headers != null) {
          for (mojom.HttpHeader header in response.headers) {
            String headerName = header.name.toLowerCase();
            String existingValue = headers[headerName];
            headers[headerName] = existingValue != null ? '$existingValue, ${header.value}' : header.value;
          }
        }
        return new Response.bytes(bodyBytes, response.statusCode, headers: headers, error: response.statusLine);
      } catch (e, stack) {
        FlutterError.reportError(new FlutterErrorDetails(
          exception: e,
          stack: stack,
          library: 'networking HTTP library',
          context: 'while interacting with the Mojo network library',
          silent: true
        ));
        rethrow;
      }
    } catch (e) {
      return new Response.bytes(null, 500, error: e);
    }
  }

  void _requireSuccess(dynamic url, int statusCode, dynamic error) {
    if (error is Exception)
      throw error;
    if (statusCode >= 400) {
      String extra;
      if (error is String && error != '') {
        extra = '\nServer response: "$error"';
      } else if (error != null) {
        extra = '\n$error';
      } else {
        extra = '';
      }
      throw new Exception('Request to "$url" failed with status $statusCode.$extra');
    }
  }

  static mojom.NetworkServiceProxy _initNetworkService() {
    return shell.connectToApplicationService('mojo:authenticated_network_service', mojom.NetworkService.connectToService);
  }

  /// A handle to the [NetworkService] object used by [MojoClient].
  static final mojom.NetworkServiceProxy networkService = _initNetworkService();
}