//   Copyright 2012 Georg-August-Universität Göttingen, Germany
//
//   Licensed under the Apache License, Version 2.0 (the "License");
//   you may not use this file except in compliance with the License.
//   You may obtain a copy of the License at
//
//       http://www.apache.org/licenses/LICENSE-2.0
//
//   Unless required by applicable law or agreed to in writing, software
//   distributed under the License is distributed on an "AS IS" BASIS,
//   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//   See the License for the specific language governing permissions and
//   limitations under the License.

package de.ugoe.cs.autoquest.httpmonitor.proxy;

import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.util.Collection;
import java.util.Enumeration;
import java.util.LinkedList;
import java.util.List;
import java.util.logging.Level;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import de.ugoe.cs.autoquest.httpmonitor.HttpMonitorExchangeHandler;
import de.ugoe.cs.autoquest.plugin.http.logdata.Address;
import de.ugoe.cs.autoquest.plugin.http.logdata.Content;
import de.ugoe.cs.autoquest.plugin.http.logdata.Cookie;
import de.ugoe.cs.autoquest.plugin.http.logdata.Cookies;
import de.ugoe.cs.autoquest.plugin.http.logdata.Header;
import de.ugoe.cs.autoquest.plugin.http.logdata.Headers;
import de.ugoe.cs.autoquest.plugin.http.logdata.HttpExchange;
import de.ugoe.cs.autoquest.plugin.http.logdata.HttpRequest;
import de.ugoe.cs.autoquest.plugin.http.logdata.HttpResponse;
import de.ugoe.cs.autoquest.plugin.http.logdata.Method;
import de.ugoe.cs.autoquest.plugin.http.logdata.ObjectFactory;
import de.ugoe.cs.autoquest.plugin.http.logdata.Protocol;
import de.ugoe.cs.autoquest.plugin.http.logdata.Status;
import de.ugoe.cs.util.console.Console;

/**
 * <p>
 * recording an exchange can not be done in one step. This is due to the fact, that the proxy
 * servlet notifies different processing states for requests and response. An exchange listener
 * records all these event. On the occurrence of the final event, it compiles an
 * {@link HttpExchange} and forwards it to the exchange handler.
 * </p>
 * 
 * @author Patrick Harms
 */
class ExchangeListener {
    
    /**
     * <p>
     * the exchange handler to forward compiles exchanges to
     * </p>
     */
    private HttpMonitorExchangeHandler exchangeHandler;
    
    /**
     * <p>
     * the request of compiled exchange
     * </p>
     */
    private HttpServletRequest request;

    /**
     * <p>
     * the content of the request of compiled exchange
     * </p>
     */
    private List<ByteBuffer> requestData = new LinkedList<ByteBuffer>();
    
    /**
     * <p>
     * the response of compiled exchange
     * </p>
     */
    private HttpServletResponse response;

    /**
     * <p>
     * the content of the response of compiled exchange
     * </p>
     */
    private List<ByteBuffer> responseData = new LinkedList<ByteBuffer>();
    
    /**
     * <p>
     * the last time an event for the exchange was received (used for supporting timeouts)
     * </p>
     */
    private long lastUpdate = System.currentTimeMillis();
    
    /**
     * <p>
     * initialized the exchange listener with the exchange handler to forward compiled exchanges to
     * </p>
     * 
     * @param exchangeHandler the exchange handler to forward compiled exchanges to
     */
    ExchangeListener(HttpMonitorExchangeHandler exchangeHandler) {
        this.exchangeHandler = exchangeHandler;
    }

    /**
     * <p>
     * called, when the request was received by the proxy
     * </p>
     * 
     * @param request the request of the exchange
     */
    public void onRequest(HttpServletRequest request) throws IllegalStateException {
        Console.traceln(Level.FINEST, this + ": onRequest " + request);

        if (request == null) {
            throw new IllegalArgumentException("request must not be null");
        }

        lastUpdate = System.currentTimeMillis();
        this.request = request;
    }

    /**
     * <p>
     * called, when some content of the request was processed by the proxy
     * </p>
     * 
     * @param data the processed content of the request of the exchange
     */
    public void onRequestContent(ByteBuffer data) {
        Console.traceln(Level.FINEST, this + ": onRequestContent " + data);

        if (data == null) {
            throw new IllegalArgumentException("data must not be null");
        }

        lastUpdate = System.currentTimeMillis();
        requestData.add(data);
    }
    
    /**
     * <p>
     * called, when the response is to be returned by the proxy
     * </p>
     * 
     * @param response the response of the exchange
     */
    public void onResponse(HttpServletResponse response) {
        Console.traceln(Level.FINEST, this + ": onResponse " + response);

        if (response == null) {
            throw new IllegalArgumentException("response must not be null");
        }

        lastUpdate = System.currentTimeMillis();
        this.response = response;
    }
    
    /**
     * <p>
     * called, when some content of the response was processed by the proxy
     * </p>
     * 
     * @param data the processed content of the response of the exchange
     */
    public void onResponseContent(ByteBuffer data) {
        Console.traceln(Level.FINEST, this + ": onResponseContent " + data);

        if (data == null) {
            throw new IllegalArgumentException("data must not be null");
        }

        lastUpdate = System.currentTimeMillis();
        responseData.add(data);
    }
    
    /**
     * <p>
     * called, when proxy finished proxying a request
     * </p>
     * 
     * @param status the status of the proxying after finalization
     */
    public void onFinish(Status status) {
        Console.traceln(Level.FINEST, this + ": onFinish " + status);

        if (status == null) {
            throw new IllegalArgumentException("status must not be null");
        }

        lastUpdate = System.currentTimeMillis();
        sendToExchangeHandler(status);
    }
    
    /**
     * @return the request of the exchange
     */
    HttpServletRequest getRequest() {
        return request;
    }

    /**
     * @return the last time this listener received an event
     */
    long getLastUpdate() {
        return lastUpdate;
    }

    /**
     * <p>
     * convenience method to compile an {@link HttpExchange} and send it to the exchange handler
     * after finalization of the exchange.
     * </p>
     * 
     * @param status the status of the proxying after finalization
     */
    private void sendToExchangeHandler(Status status) {
        ObjectFactory eventObjectFactory = new ObjectFactory();
        HttpExchange exchange = eventObjectFactory.createHttpExchange();
        
        exchange.setStatus(status);
        
        // the remote address
        Address address = eventObjectFactory.createAddress();
        address.setIp(request.getRemoteAddr());
        address.setHost(request.getRemoteHost());
        address.setPort(BigInteger.valueOf(request.getRemotePort()));
        exchange.setSender(address);
        
        // the local address
        address = eventObjectFactory.createAddress();
        address.setIp(request.getLocalAddr());
        address.setHost(request.getLocalName());
        address.setPort(BigInteger.valueOf(request.getLocalPort()));
        exchange.setReceiver(address);
        
        exchange.setRequest(map(request, eventObjectFactory));
        exchange.setResponse(map(response, eventObjectFactory));
        
        exchangeHandler.handleHttpExchange(exchange);
    }

    /**
     * <p>
     * convenience method to map an {@link HttpServletRequest} to an {@link HttpRequest}
     * </p>
     */
    private HttpRequest map(HttpServletRequest request, ObjectFactory eventObjectFactory) {
        HttpRequest eventRequest = eventObjectFactory.createHttpRequest();
        eventRequest.setMethod(Method.fromValue(request.getMethod()));
        eventRequest.setProtocol(Protocol.fromValue(request.getProtocol()));
        eventRequest.setUrl(request.getRequestURL().toString());
        eventRequest.setQuery(request.getQueryString());
        
        Headers headers = eventObjectFactory.createHeaders();
        Enumeration<String> headerNames = request.getHeaderNames();
        while (headerNames.hasMoreElements()) {
            String headerName = headerNames.nextElement();
            
            Enumeration<String> headerValues = request.getHeaders(headerName);
            while (headerValues.hasMoreElements()) {
                Header header = eventObjectFactory.createHeader();
                header.setKey(headerName);
                header.setValue(headerValues.nextElement());
                headers.getHeader().add(header);
            }
        }
        eventRequest.setHeaders(headers);
        
        if (request.getCookies() != null) {
            Cookies cookies = eventObjectFactory.createCookies();
            for (javax.servlet.http.Cookie requestCookie : request.getCookies()) {
                Cookie cookie = eventObjectFactory.createCookie();
                cookie.setComment(requestCookie.getComment());
                cookie.setDomain(requestCookie.getDomain());
                cookie.setIsHttpOnly(requestCookie.isHttpOnly());
                cookie.setIsSecure(requestCookie.getSecure());
                cookie.setMaxAge(BigInteger.valueOf(requestCookie.getMaxAge()));
                cookie.setName(requestCookie.getName());
                cookie.setPath(requestCookie.getPath());
                cookie.setValue(requestCookie.getValue());
                cookie.setVersion(BigInteger.valueOf(requestCookie.getVersion()));
                cookies.getCookie().add(cookie);
            }
            eventRequest.setCookies(cookies);
        }
        
        eventRequest.setAuthType(request.getAuthType());
        eventRequest.setRemoteUser(request.getRemoteUser());
        eventRequest.setRequestedSessionId(request.getRequestedSessionId());
        
        if (requestData.size() > 0) {
            Content content = eventObjectFactory.createContent();
            content.setEncoding(request.getCharacterEncoding());
            content.setType(request.getContentType());
            content.setLength(request.getContentLength());
            content.setData(createString(requestData));
            eventRequest.setContent(content);
        }
        
        return eventRequest;
    }

    /**
     * <p>
     * convenience method to map an {@link HttpServletResponse} to an {@link HttpResponse}
     * </p>
     */
    private HttpResponse map(HttpServletResponse response, ObjectFactory eventObjectFactory) {
        HttpResponse eventResponse = eventObjectFactory.createHttpResponse();
        
        eventResponse.setStatus(BigInteger.valueOf(response.getStatus()));

        Headers headers = eventObjectFactory.createHeaders();
        Collection<String> headerNames = response.getHeaderNames();
        for (String headerName : headerNames) {
            Collection<String> headerValues = response.getHeaders(headerName);
            for (String headerValue : headerValues) {
                Header header = eventObjectFactory.createHeader();
                header.setKey(headerName);
                header.setValue(headerValue);
                headers.getHeader().add(header);
            }
        }
        eventResponse.setHeaders(headers);
        
        if (responseData.size() > 0) {
            Content content = eventObjectFactory.createContent();
            content.setEncoding(response.getCharacterEncoding());
            content.setType(response.getContentType());

            String data = createString(responseData);
            content.setLength(data.length());
            content.setData(data);
            
            eventResponse.setContent(content);
        }
        
        return eventResponse;
    }

    /**
     * <p>
     * convenience method to create a string out of recorded request or response content
     * </p>
     */
    private String createString(List<ByteBuffer> bufferList) {
        StringBuffer str = new StringBuffer();
        
        for (ByteBuffer buffer : bufferList) {
            while (buffer.hasRemaining()) {
                str.append((char) buffer.get());
            }
        }
        
        return str.toString();
    }

}
