/*
 * Copyright 2014 Red Hat, Inc.
 *
 *  All rights reserved. This program and the accompanying materials
 *  are made available under the terms of the Eclipse Public License v1.0
 *  and Apache License v2.0 which accompanies this distribution.
 *
 *  The Eclipse Public License is available at
 *  http://www.eclipse.org/legal/epl-v10.html
 *
 *  The Apache License v2.0 is available at
 *  http://www.opensource.org/licenses/apache2.0.php
 *
 *  You may elect to redistribute this code under either of these licenses.
 */

package io.vertx.ext.web.handler.impl;

import java.util.List;
import java.util.Objects;
import java.util.Timer;
import java.util.TimerTask;

import io.vertx.core.Handler;
import io.vertx.core.http.HttpHeaders;
import io.vertx.core.http.HttpServerRequest;
import io.vertx.core.http.HttpServerResponse;
import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.web.MIMEHeader;
import io.vertx.ext.web.RoutingContext;
import io.vertx.ext.web.handler.ErrorHandler;
import io.vertx.ext.web.impl.HeaderTooLongException;
import io.vertx.ext.web.impl.Utils;

/**
 * @author <a href="http://pmlopes@gmail.com">Paulo Lopes</a>
 * @author <a href="http://tfox.org">Tim Fox</a>
 */
public class ErrorHandlerImpl implements ErrorHandler {

  /**
   * Flag to enable/disable printing the full stack trace of exceptions.
   */
  private final boolean displayExceptionDetails;

  /**
   * Cached template for rendering the html errors
   */
  private final String errorTemplate;

  public ErrorHandlerImpl(String errorTemplateName, boolean displayExceptionDetails) {
    Objects.requireNonNull(errorTemplateName);
    this.displayExceptionDetails = displayExceptionDetails;
    this.errorTemplate = Utils.readResourceToBuffer(errorTemplateName).toString();
  }

  @Override
  public void handle(RoutingContext context) {

    HttpServerRequest request = context.request();
    HttpServerResponse response = context.response();

    Throwable failure = context.failure();
    
    if(answerWithKnownFailure(context)){
      return;
    }
    
    int errorCode = context.statusCode();
    String errorMessage = null;
    if (errorCode != -1) {
      context.response().setStatusCode(errorCode);
      errorMessage = context.response().getStatusMessage();
    } else {
      errorCode = 500;
      if (displayExceptionDetails) {
        errorMessage = failure.getMessage();
      }
      if (errorMessage == null) {
        errorMessage = "Internal Server Error";
      }
      // no new lines are allowed in the status message
      response.setStatusMessage(errorMessage.replaceAll("\\r|\\n", " "));
    }
    
    answerWithError(context, errorCode, errorMessage);
  }
  
  private boolean answerWithKnownFailure(RoutingContext context) {
    Throwable failure = context.failure();
    
    if(failure instanceof HeaderTooLongException){
      // Math.random is random enough for this purpose. Will wait between 1 ms and 11ms
      
      context.vertx().setTimer(1 + (long)(Math.random() * 10), 
          new DelayedAnswer(context, 400, "A header was too long to process")
      );
          
      return true;
    }
    return false;
  }

  private void answerWithError(RoutingContext context, int errorCode, String errorMessage){
    context.response().setStatusCode(errorCode);
    if( !sendErrorResponseMIME(context, errorCode, errorMessage) &&
        !sendErrorAcceptMIME(context, errorCode, errorMessage)
      ){
      // fallback plain/text
      sendError(context, "text/plain", errorCode, errorMessage);
    }
  }
  
  private boolean sendErrorResponseMIME(RoutingContext context, int errorCode, String errorMessage){
    // does the response already set the mime type?
    String mime = context.response().headers().get(HttpHeaders.CONTENT_TYPE);
    
    return mime != null && sendError(context, mime, errorCode, errorMessage);
  }
  
  private boolean sendErrorAcceptMIME(RoutingContext context, int errorCode, String errorMessage){
    // respect the client accept order
    List<MIMEHeader> acceptableMimes = context.parsedHeaders().accept();

    for (MIMEHeader accept : acceptableMimes) {
      if (sendError(context, accept.value(), errorCode, errorMessage)) {
        return true;
      }
    }
    return false;
  }

  private boolean sendError(RoutingContext context, String mime, int errorCode, String errorMessage) {

    final String title = "Matron!";

    HttpServerResponse response = context.response();

    if (mime.startsWith("text/html")) {
      StringBuilder stack = new StringBuilder();
      if (context.failure() != null && displayExceptionDetails) {
        for (StackTraceElement elem: context.failure().getStackTrace()) {
          stack.append("<li>").append(elem).append("</li>");
        }
      }
      response.putHeader(HttpHeaders.CONTENT_TYPE,"text/html");
      response.end(
        errorTemplate.replace("{title}", title)
          .replace("{errorCode}", Integer.toString(errorCode))
          .replace("{errorMessage}", errorMessage)
          .replace("{stackTrace}", stack.toString())
      );
      return true;
    }

    if (mime.startsWith("application/json")) {
      JsonObject jsonError = new JsonObject();
      jsonError.put("error", new JsonObject().put("code", errorCode).put("message", errorMessage));
      if (context.failure() != null && displayExceptionDetails) {
        JsonArray stack = new JsonArray();
        for (StackTraceElement elem: context.failure().getStackTrace()) {
          stack.add(elem.toString());
        }
        jsonError.put("stack", stack);
      }
      response.putHeader(HttpHeaders.CONTENT_TYPE, "application/json");
      response.end(jsonError.encode());
      return true;
    }

    if (mime.startsWith("text/plain")) {
      response.putHeader(HttpHeaders.CONTENT_TYPE, "text/plain");
      StringBuilder sb = new StringBuilder();
      sb.append("Error ");
      sb.append(errorCode);
      sb.append(": ");
      sb.append(errorMessage);
      if (context.failure() != null && displayExceptionDetails) {
        for (StackTraceElement elem: context.failure().getStackTrace()) {
          sb.append("\tat ").append(elem).append("\n");
        }
      }
      response.end(sb.toString());
      return true;
    }

    return false;
  }
  
  /**
   * This class is used to delay the answer to a potential attacker<br>
   * In practice, this simulates heavy duty work while, in reality, it was just waiting.
   */
  private class DelayedAnswer implements Handler<Long>{
    
    RoutingContext context;
    int errorCode;
    String errorMessage;
    private DelayedAnswer(RoutingContext context, int errorCode, String errorMessage) {
      this.context = context;
      this.errorCode = errorCode;
      this.errorMessage = errorMessage;
    }
    @Override
    public void handle(Long arg0) {
      ErrorHandlerImpl.this.answerWithError(context, errorCode, errorMessage);
    }
  }
}

@@ -611,60 +611,8 @@
 cts;
-%0Aimport java.util.Timer;%0Aimport java.util.TimerTask;
 %0A%0Aim
@@ -682,53 +682,8 @@
 rs;%0A
-import io.vertx.core.http.HttpServerRequest;%0A
 impo
@@ -1770,59 +1770,8 @@
  %7B%0A%0A
-    HttpServerRequest request = context.request();%0A
     
@@ -2742,187 +2742,22 @@
     
-// Math.random is random enough for this purpose. Will wait between 1 ms and 11ms%0A      %0A      context.vertx().setTimer(1 + (long)(Math.random() * 10), %0A          new DelayedAnswe
+answerWithErro
 r(co
@@ -2807,28 +2807,9 @@
 ss%22)
-%0A      );%0A          
+;
 %0A   
@@ -5934,24 +5934,24 @@
 rue;%0A    %7D%0A%0A
+
     return f
@@ -5964,652 +5964,6 @@
   %7D%0A
-  %0A  /**%0A   * This class is used to delay the answer to a potential attacker%3Cbr%3E%0A   * In practice, this simulates heavy duty work while, in reality, it was just waiting.%0A   */%0A  private class DelayedAnswer implements Handler%3CLong%3E%7B%0A    %0A    RoutingContext context;%0A    int errorCode;%0A    String errorMessage;%0A    private DelayedAnswer(RoutingContext context, int errorCode, String errorMessage) %7B%0A      this.context = context;%0A      this.errorCode = errorCode;%0A      this.errorMessage = errorMessage;%0A    %7D%0A    @Override%0A    public void handle(Long arg0) %7B%0A      ErrorHandlerImpl.this.answerWithError(context, errorCode, errorMessage);%0A    %7D%0A  %7D%0A
 %7D%0A

Remove delay. They don't want it.