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

import 'log/log.dart';
import 'page/log_page.dart';
import 'page/stats_page.dart';

/// An exception that is thrown when a request is received that cannot be
/// handled.
class UnknownRequest implements Exception {}

/// A simple web server.
class WebServer {
  /// The path to the page containing a single page from the instrumentation
  /// log.
  static final String logPath = '/log';

  /// The path to the page containing statistics about the instrumentation log.
  static final String statsPath = '/stats';

  /// The content type for HTML responses.
  static final ContentType _htmlContent = ContentType(
    'text',
    'html',
    charset: 'utf-8',
  );

  /// The instrumentation log being served up.
  final InstrumentationLog log;

  /// Future that is completed with the HTTP server once it is running.
  late Future<HttpServer> _server;

  /// The number of log entries to show on a result page.
  final int pageLength;

  /// Initialize a newly created server.
  new(this.log, {required this.pageLength});

  Map<String, String> getParameterMap(HttpRequest request) {
    Map<String, String> parameterMap = HashMap<String, String>();
    var query = request.uri.query;
    if (query.isNotEmpty) {
      var pairs = query.split('&');
      for (var pair in pairs) {
        var parts = pair.split('=');
        var value = parts[1].trim();
        value = value.replaceAll('+', ' ');
        parameterMap[parts[0].trim()] = value;
      }
    }
    return parameterMap;
  }

  /// Return a table mapping the names of properties to the values of those
  /// properties that is extracted from the given HTTP [request].
  Future<Map<String, String>> getValueMap(HttpRequest request) async {
    var buffer = StringBuffer();
    await request.forEach((List<int> element) {
      for (var code in element) {
        buffer.writeCharCode(code);
      }
    });
    Map<String, String> valueMap = HashMap<String, String>();
    var parameters = buffer.toString();
    if (parameters.isNotEmpty) {
      var pairs = parameters.split('&');
      for (var pair in pairs) {
        var parts = pair.split('=');
        var value = parts[1].trim();
        value = value.replaceAll('+', ' ');
        valueMap[parts[0].trim()] = value;
      }
    }
    return valueMap;
  }

  /// Begin serving HTTP requests over the given [port].
  void serveHttp(int port) {
    _server = HttpServer.bind(InternetAddress.loopbackIPv4, port);
    _server.then(_handleServer).catchError((_) {
      /* Ignore errors. */
    });
  }

  /// Handle a GET [request] received by the HTTP server.
  void _handleGetRequest(HttpRequest request) {
    var buffer = StringBuffer();
    try {
      var path = request.uri.path;
      if (path == logPath) {
        _writeLogPage(request, buffer);
      } else if (path == statsPath) {
        _writeStatsPage(request, buffer);
      } else {
        _returnUnknownRequest(request);
        return;
      }
    } on UnknownRequest {
      _returnUnknownRequest(request);
      return;
    } catch (exception, stackTrace) {
      var response = request.response;
      response.statusCode = HttpStatus.ok;
      response.headers.contentType = _htmlContent;
      var buffer = StringBuffer();
      buffer.write('<p><b>Exception while composing page:</b></p>');
      buffer.write('<p>$exception</p>');
      buffer.write('<p>');
      _writeStackTrace(buffer, stackTrace);
      buffer.write('</p>');
      response.write(buffer.toString());
      response.close();
      return;
    }

    var response = request.response;
    response.statusCode = HttpStatus.ok;
    response.headers.contentType = _htmlContent;
    response.write(buffer.toString());
    response.close();
  }

  /// Handle a POST [request] received by the HTTP server.
  Future<void> _handlePostRequest(HttpRequest request) async {
    _returnUnknownRequest(request);
  }

  /// Attach a listener to a newly created HTTP server.
  void _handleServer(HttpServer httpServer) {
    httpServer.listen((HttpRequest request) {
      var method = request.method;
      if (method == 'GET') {
        _handleGetRequest(request);
      } else if (method == 'POST') {
        _handlePostRequest(request);
      } else {
        _returnUnknownRequest(request);
      }
    });
  }

  /// Return an error in response to an unrecognized request received by the
  /// HTTP server.
  void _returnUnknownRequest(HttpRequest request) {
    var response = request.response;
    response.statusCode = HttpStatus.notFound;
    response.headers.contentType = ContentType(
      'text',
      'html',
      charset: 'utf-8',
    );
    response.write(
      '<html><head></head><body><h3>Page not found: "${request.uri.path}".</h3></body></html>',
    );
    response.close();
  }

  void _writeLogPage(HttpRequest request, StringBuffer buffer) {
    var parameterMap = getParameterMap(request);
    var groupId = parameterMap['group'];
    var startIndex = parameterMap['start'];
    var page = LogPage(log);
    page.selectedGroup = EntryGroup.withId(groupId ?? 'nonTask')!;
    if (startIndex != null) {
      page.pageStart = int.parse(startIndex);
    } else {
      page.pageStart = 0;
    }
    page.pageLength = pageLength;
    page.writePage(buffer);
  }

  /// Write a representation of the given [stackTrace] to the given [sink].
  void _writeStackTrace(StringSink sink, StackTrace stackTrace) {
    var trace = stackTrace.toString().replaceAll('#', '<br>#');
    if (trace.startsWith('<br>#')) {
      trace = trace.substring(4);
    }
    sink.write('<p>');
    sink.write(trace);
    sink.write('</p>');
  }

  void _writeStatsPage(HttpRequest request, StringBuffer buffer) {
    StatsPage(log).writePage(buffer);
  }
}
