// 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:async';
import 'dart:collection';
import 'dart:io';

import 'log/log.dart';
import 'page/log_page.dart';
import 'page/stats_page.dart';
import 'page/task_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 path to the page containing statistics about the instrumentation log.
   */
  static final String taskPath = '/task';

  /**
   * The content type for HTML responses.
   */
  static final ContentType _htmlContent =
      new 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.
   */
  Future<HttpServer> _server;

  /**
   * Initialize a newly created server.
   */
  WebServer(this.log);

  Map<String, String> getParameterMap(HttpRequest request) {
    Map<String, String> parameterMap = new HashMap<String, String>();
    String query = request.uri.query;
    if (query != null && query.isNotEmpty) {
      List<String> pairs = query.split('&');
      for (String pair in pairs) {
        List<String> parts = pair.split('=');
        String 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 {
    StringBuffer buffer = new StringBuffer();
    await request.forEach((List<int> element) {
      for (int code in element) {
        buffer.writeCharCode(code);
      }
    });
    Map<String, String> valueMap = new HashMap<String, String>();
    String parameters = buffer.toString();
    if (parameters.isNotEmpty) {
      List<String> pairs = parameters.split('&');
      for (String pair in pairs) {
        List<String> parts = pair.split('=');
        String 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) {
    StringBuffer buffer = new StringBuffer();
    try {
      String path = request.uri.path;
      if (path == logPath) {
        _writeLogPage(request, buffer);
      } else if (path == statsPath) {
        _writeStatsPage(request, buffer);
      } else if (path == taskPath) {
        _writeTaskPage(request, buffer);
      } else {
        _returnUnknownRequest(request);
        return;
      }
    } on UnknownRequest {
      _returnUnknownRequest(request);
      return;
    } catch (exception, stackTrace) {
      HttpResponse response = request.response;
      response.statusCode = HttpStatus.ok;
      response.headers.contentType = _htmlContent;
      StringBuffer buffer = new 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;
    }

    HttpResponse 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<Null> _handlePostRequest(HttpRequest request) async {
    _returnUnknownRequest(request);
  }

  /**
   * Attach a listener to a newly created HTTP server.
   */
  void _handleServer(HttpServer httpServer) {
    httpServer.listen((HttpRequest request) {
      String 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) {
    HttpResponse response = request.response;
    response.statusCode = HttpStatus.notFound;
    response.headers.contentType =
        new 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) {
    Map<String, String> parameterMap = getParameterMap(request);
    String groupId = parameterMap['group'];
    String startIndex = parameterMap['start'];
    LogPage page = new LogPage(log);
    page.selectedGroup = EntryGroup.withId(groupId ?? 'nonTask');
    if (startIndex != null) {
      page.pageStart = int.parse(startIndex);
    } else {
      page.pageStart = 0;
    }
    page.pageLength = 25;
    page.writePage(buffer);
  }

  /**
   * Write a representation of the given [stackTrace] to the given [sink].
   */
  void _writeStackTrace(StringSink sink, StackTrace stackTrace) {
    if (stackTrace != null) {
      String 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) {
    new StatsPage(log).writePage(buffer);
  }

  void _writeTaskPage(HttpRequest request, StringBuffer buffer) {
    Map<String, String> parameterMap = getParameterMap(request);
    String analysisStart = parameterMap['analysisStart'];
    String start = parameterMap['start'];
    TaskPage page = new TaskPage(log);
    if (analysisStart == null) {
      throw new UnknownRequest();
    }
    page.analysisStart = int.parse(analysisStart);
    if (start != null) {
      page.pageStart = int.parse(start);
    } else {
      page.pageStart = 0;
    }
    page.pageLength = 25;
    page.writePage(buffer);
  }
}
