package com.neverwinterdp.sparkngin.http;

import io.netty.handler.codec.http.HttpContent;
import io.netty.handler.codec.http.HttpResponse;
import io.netty.handler.codec.http.QueryStringEncoder;

import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.TimeoutException;

import com.neverwinterdp.message.Message;
import com.neverwinterdp.netty.http.client.AsyncHttpClient;
import com.neverwinterdp.netty.http.client.ResponseHandler;
import com.neverwinterdp.sparkngin.Ack;

abstract public class AbstractHttpSparknginClient {
  private String path = "/message/json" ;
  private AsyncHttpClient client ;
  private LinkedHashMap<String, Message> waitingAckMessages ;
  private int bufferSize ;
  private int sendCount = 0;
  private int errorCount = 0;
  
  public AbstractHttpSparknginClient(String host, int port, int bufferSize, boolean connect) throws Exception {
    client = new AsyncHttpClient(host, port, new MessageResponseHandler(), connect) ;
    this.bufferSize = bufferSize ;
    waitingAckMessages = new LinkedHashMap<String, Message>() ;
  }
  
  public boolean isConnected() { return client.isConnected() ; }
  
  public void setNotConnected() { client.setNotConnected(); }
  
  public boolean connect() throws Exception {
    return client.connect();
  }
  
  public boolean connect(long timeout, long tryPeriod) throws Exception {
    return client.connect(timeout, tryPeriod);
  }
  
  public void setPath(String path) { this.path = path ; }
  
  public int getSendCount() { return this.sendCount ; }
  
  public int getErrorCount() { return errorCount ;}
  
  public void sendGet(Message message, long timeout) throws Exception {
    synchronized(waitingAckMessages) {
      if(waitingAckMessages.size() >= bufferSize) {
        waitingAckMessages.wait(timeout);
        if(waitingAckMessages.size() >= bufferSize) {
          throw new TimeoutException("fail to send the message in " + timeout + "ms") ;
        }
      }
      QueryStringEncoder encoder = new QueryStringEncoder(path);
      encoder.addParam("data", toStringData(message));
      client.get(encoder.toString());
      sendCount++ ;
      String messageId = message.getHeader().getKey() ;
      waitingAckMessages.put(messageId, message) ;
    }
  }
  
  public void sendPost(Message message, long timeout) throws Exception {
    synchronized(waitingAckMessages) {
      if(waitingAckMessages.size() >= bufferSize) {
        waitingAckMessages.wait(timeout);
        if(waitingAckMessages.size() >= bufferSize) {
          throw new TimeoutException("fail to send the message in " + timeout + "ms") ;
        }
      }
      client.post(path, toBinData(message));
      sendCount++ ;
      String messageId = message.getHeader().getKey() ;
      waitingAckMessages.put(messageId, message) ;
    }
  }
  
  public void onFailedMessage(Ack ack, Message message) {
    errorCount++ ;
    System.out.println("Failed message: " + ack.getMessageId() + ", message = " + ack.getMessage());
  }
  
  public Map<String, Message> getWaitingMessages() { 
    return this.waitingAckMessages ;
  }
  
  public void waitAndClose(long waitTime) throws InterruptedException {
    if(waitingAckMessages.size() > 0) { 
      synchronized(waitingAckMessages) {
        long stopTime = System.currentTimeMillis() + waitTime ;
        while(stopTime > System.currentTimeMillis() && waitingAckMessages.size() > 0) {
          long timeToWait = stopTime - System.currentTimeMillis() ;
          waitingAckMessages.wait(timeToWait);
        }
      }
    }
    close();
  }
  
  public void close() {
    if(waitingAckMessages.size() > 0) {
      System.err.println("There are " + waitingAckMessages.size() + " messages waitting for ack") ;
    }
    client.close(); 
  }
  
  abstract protected byte[] toBinData(Message message)  ;
  abstract protected String toStringData(Message message)  ;
  abstract protected Ack    toAck(HttpContent content)  ;
  
  protected class MessageResponseHandler implements ResponseHandler {
    public void onResponse(HttpResponse response) {
      if(response instanceof HttpContent) {
        Ack ack = toAck((HttpContent) response) ;
        String messageId = (String) ack.getMessageId() ;
        Message message = waitingAckMessages.get(messageId) ;
        if(!Ack.Status.OK.equals(ack.getStatus())) {
          onFailedMessage(ack, message) ;
        }
        synchronized(waitingAckMessages) {
          waitingAckMessages.remove(messageId) ;
          waitingAckMessages.notify() ;
        }
      }
    }
  }
}