package org.jboss.seam.remoting;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

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

import org.dom4j.Document;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import org.jboss.seam.contexts.RemotingLifecycle;
import org.jboss.seam.core.ConversationPropagation;
import org.jboss.seam.core.Manager;
import org.jboss.seam.log.LogProvider;
import org.jboss.seam.log.Logging;
import org.jboss.seam.remoting.wrapper.Wrapper;
import org.jboss.seam.servlet.ContextualHttpServletRequest;

/**
 * Unmarshals the calls from an HttpServletRequest, executes them in order and
 * marshals the responses.
 *
 * @author Shane Bryzak
 */
public class ExecutionHandler extends BaseRequestHandler implements RequestHandler
{
   private static final LogProvider log = Logging.getLogProvider(ExecutionHandler.class);

  private static final byte[] HEADER_OPEN = "<header>".getBytes();
  private static final byte[] HEADER_CLOSE = "</header>".getBytes();
  private static final byte[] CONVERSATION_ID_TAG_OPEN = "<conversationId>".getBytes();
  private static final byte[] CONVERSATION_ID_TAG_CLOSE = "</conversationId>".getBytes();

  private static final byte[] CONTEXT_TAG_OPEN = "<context>".getBytes();
  private static final byte[] CONTEXT_TAG_CLOSE = "</context>".getBytes();

  /**
   * The entry point for handling a request.
   *
   * @param request HttpServletRequest
   * @param response HttpServletResponse
   * @throws Exception
   */
  public void handle(HttpServletRequest request, final HttpServletResponse response)
      throws Exception
  {
      // We're sending an XML response, so set the response content type to text/xml
      response.setContentType("text/xml");
      
      ByteArrayOutputStream out = new ByteArrayOutputStream();
      
      byte[] buffer = new byte[256];
      int read = request.getInputStream().read(buffer);
      while (read != -1)
      {
         out.write(buffer, 0, read);
         read = request.getInputStream().read(buffer);
      }
      
      String requestData = new String(out.toByteArray());
      log.debug("Processing remote request: " + requestData);
      
      // Parse the incoming request as XML
      SAXReader xmlReader = new SAXReader();
      Document doc = xmlReader.read( new StringReader(requestData) );
      final Element env = doc.getRootElement();
      final RequestContext ctx = unmarshalContext(env);

      // TODO - we really want to extract the page context from our request
      RemotingLifecycle.restorePageContext();

      new ContextualHttpServletRequest(request)
      {
         @Override
         public void process() throws Exception
         {
            // Extract the calls from the request            
            List<Call> calls = unmarshalCalls(env);

            // Execute each of the calls
            for (Call call : calls) 
            {
               call.execute();
            }               

            // Store the conversation ID in the outgoing context
            ctx.setConversationId( Manager.instance().getCurrentConversationId() );               
            
            // Package up the response
            marshalResponse(calls, ctx, response.getOutputStream());               
         }
         
         @Override
         protected void restoreConversationId()
         {
            ConversationPropagation.instance().setConversationId( ctx.getConversationId() );
         }
         
         @Override
         protected void handleConversationPropagation() {}
         
      }.run();
      
  }

  /**
   * Unmarshals the context from the request envelope header.
   *
   * @param env Element
   * @return RequestContext
   */
  private RequestContext unmarshalContext(Element env)
  {
    RequestContext ctx = new RequestContext();

    Element header = env.element("header");
    if (header != null)
    {
      Element context = header.element("context");
      if (context != null)
      {

        Element convId = context.element("conversationId");
        if (convId != null)
        {
          ctx.setConversationId(convId.getText());
        }
      }
    }

    return ctx;
  }

  /**
   * Unmarshal the request into a list of Calls.
   *
   * @param env Element
   * @throws Exception
   */
  private List<Call> unmarshalCalls(Element env) throws Exception
  {
    try 
    {
      List<Call> calls = new ArrayList<Call>();

      List<Element> callElements = env.element("body").elements("call");

      for (Element e : callElements) 
      {
        Call call = new Call(e.attributeValue("id"),
                             e.attributeValue("component"),
                             e.attributeValue("method"));

        // First reconstruct all the references
        Element refsNode = e.element("refs");

        Iterator iter = refsNode.elementIterator("ref");
        while (iter.hasNext())
        {
          call.getContext().createWrapperFromElement((Element) iter.next());
        }

        // Now unmarshal the ref values
        for (Wrapper w : call.getContext().getInRefs().values())
        {
          w.unmarshal();
        }

        Element paramsNode = e.element("params");

        // Then process the param values
        iter = paramsNode.elementIterator("param");
        while (iter.hasNext()) 
        {
          Element param = (Element) iter.next();

          call.addParameter(call.getContext().createWrapperFromElement(
            (Element) param.elementIterator().next()));
        }

        calls.add(call);
      }

      return calls;
    }
    catch (Exception ex) 
    {
      log.error("Error unmarshalling calls from request", ex);
      throw ex;
    }
  }

  /**
   * Write the results to the output stream.
   *
   * @param calls List The list of calls to write
   * @param out OutputStream The stream to write to
   * @throws IOException
   */
  private void marshalResponse(List<Call> calls, RequestContext ctx, OutputStream out)
      throws IOException
  {
    out.write(ENVELOPE_TAG_OPEN);

    if (ctx.getConversationId() != null)
    {
      out.write(HEADER_OPEN);
      out.write(CONTEXT_TAG_OPEN);
      out.write(CONVERSATION_ID_TAG_OPEN);
      out.write(ctx.getConversationId().getBytes());
      out.write(CONVERSATION_ID_TAG_CLOSE);
      out.write(CONTEXT_TAG_CLOSE);
      out.write(HEADER_CLOSE);
    }

    out.write(BODY_TAG_OPEN);

    for (Call call : calls)
    {
      MarshalUtils.marshalResult(call, out);
    }

    out.write(BODY_TAG_CLOSE);
    out.write(ENVELOPE_TAG_CLOSE);
    out.flush();
  }
}
