// ---------------------------------------------------------------------------
// - HttpRequest.cpp                                                         -
// - afnix:nwg module - http request class implementation                    -
// ---------------------------------------------------------------------------
// - This program is free software;  you can redistribute it  and/or  modify -
// - it provided that this copyright notice is kept intact.                  -
// -                                                                         -
// - This program  is  distributed in  the hope  that it will be useful, but -
// - without  any  warranty;  without  even   the   implied    warranty   of -
// - merchantability or fitness for a particular purpose.  In no event shall -
// - the copyright holder be liable for any  direct, indirect, incidental or -
// - special damages arising in any way out of the use of this software.     -
// ---------------------------------------------------------------------------
// - copyright (c) 1999-2011 amaury darsch                                   -
// ---------------------------------------------------------------------------

#include "Regex.hpp"
#include "Vector.hpp"
#include "Strvec.hpp"
#include "Utility.hpp"
#include "Runnable.hpp"
#include "QuarkZone.hpp"
#include "Exception.hpp"
#include "HttpRequest.hpp"

namespace afnix {

  // -------------------------------------------------------------------------
  // - private section                                                       -
  // -------------------------------------------------------------------------

  // the nil http version
  static const String HTTP_VERS_NULL = "";
  // the default http request uri
  static const String HTTP_RURI_NULL = "";
  // the default http request uri
  static const String HTTP_RURI_XDEF = "/";
  // the default http options uri
  static const String HTTP_RURI_OPTS = "*";
  // the standard request methods
  static const String HTTP_RMTH_XGET = "GET";  
  static const String HTTP_RMTH_HEAD = "HEAD";  
  static const String HTTP_RMTH_POST = "POST";  
  static const String HTTP_RMTH_TRCE = "TRACE";  
  static const String HTTP_RMTH_OPTS = "OPTIONS";  
  // the default http method
  static const String HTTP_RMTH_XDEF = HTTP_RMTH_XGET;
  // the http content length attribute
  static const String HTTP_CLEN_ATTR = "Content-Length";
  // the http connection type attribute
  static const String HTTP_CTYP_ATTR = "Content-Type";
  // the http host attribute
  static const String HTTP_HOST_ATTR = "Host";
  // the http connection attribute
  static const String HTTP_CONN_ATTR = "Connection";
  // the http connection value
  static const String HTTP_CONN_XDEF = "close";
  // the form url encoded type
  static const String HTTP_CTYP_FORM = "application/x-www-form-urlencoded";
  // the maximum pos content-length with form
  static const long   HTTP_CLEN_MSIZ = 8192;

  // this procedure returns the default request uri by method
  static String http_rmth_ruri (const String rmth) {
    if (rmth == HTTP_RMTH_XGET) return HTTP_RURI_XDEF;
    if (rmth == HTTP_RMTH_HEAD) return HTTP_RURI_XDEF;
    if (rmth == HTTP_RMTH_POST) return HTTP_RURI_XDEF;
    if (rmth == HTTP_RMTH_TRCE) return HTTP_RURI_XDEF;
    if (rmth == HTTP_RMTH_OPTS) return HTTP_RURI_OPTS;
    return HTTP_RURI_NULL;
  }

  // this procedure maps a uri to a request uri
  static String http_uri_ruri (const Uri& uri) {
    String result = uri.getpenc ();
    if (result.isnil () == true) result = HTTP_RURI_XDEF;
    return result;
  }

  // this procedure parse a request line
  static Strvec http_parse_request (InputStream& is) {
    // check for valid stream
    if (is.valid () == false) {
      throw Exception ("http-error", "http request timeout");
    }
    // get the request line
    String line = is.readln ();
    // split the components
    Strvec result = Strvec::split (line);
    // check length and protocol
    if ((result.length () == 0) || (result.length () > 3)) {
      throw Exception ("http-error", "invalid request line", line);
    }
    // here is the result
    return result;
  }

  // this procedure parse a query string if any
  static String http_parse_query (const String& rmth, const String& ruri,
				  const Plist& head,  InputStream& is) {
    // prepare result
    String result;
    // eventually normalize the request uri
    String nuri = "http:";
    Regex  suri ("($l$a*):($N+)");
    nuri = (suri == ruri) ? ruri : nuri + ruri;
    // process the get method
    if (rmth ==  HTTP_RMTH_XGET) {
      // create a uri by request uri
      Uri uri (nuri);
      // extract the query string
      result = uri.getquery ();
    }
    // process the post method
    if (rmth ==  HTTP_RMTH_POST) {
      // check if the content type is application/x-www-form-urlencoded
      if (head.exists (HTTP_CTYP_ATTR) == false) return result;
      if (head.getpval (HTTP_CTYP_ATTR) != HTTP_CTYP_FORM) return result;
      // check if we have a content length
      if (head.exists (HTTP_CLEN_ATTR) == false) return result;
      long clen = head.tolong (HTTP_CLEN_ATTR);
      if (clen > HTTP_CLEN_MSIZ) {
	throw Exception ("http-error", "content length is too large");
      }
      // read the string as the content length
      char rbuf[clen+1];
      if (is.copy (rbuf, clen) != clen) {
	throw Exception ("http-error", "cannot extract port query string");
      }
      rbuf[clen] = nilc;
      result = rbuf;
    }
    return result;
  }

  // -------------------------------------------------------------------------
  // - class section                                                         -
  // -------------------------------------------------------------------------
  
  // create a default request
  
  HttpRequest::HttpRequest (void) : HttpProto (HTTP_CLIENT) {
    reset ();
  }

  // create a http request by method
  
  HttpRequest::HttpRequest (const String& rmth) : HttpProto (HTTP_CLIENT) {
    // reset protocol
    reset ();
    // set the request data
    d_rmth = rmth;
    d_ruri = http_rmth_ruri (d_rmth);
  }
  
  // create a http request by uri

  HttpRequest::HttpRequest (const Uri& uri) : HttpProto (HTTP_CLIENT) {
    // reset protocol
    reset ();
    // set the request data
    d_rmth = HTTP_RMTH_XDEF;  
    d_ruri = http_uri_ruri (uri);
    // set the default header
    hset (HTTP_HOST_ATTR, uri.getauth ());
    hset (HTTP_CONN_ATTR, HTTP_CONN_XDEF);
  }

  // create a http request by method and uri name
  
  HttpRequest::HttpRequest (const String& rmth,
			    const String& ruri) : HttpProto (HTTP_CLIENT) {
    // reset protocol
    reset ();
    // set the request data
    d_rmth = rmth;
    d_ruri = ruri;
  }
  
  // create a http request by method and uri

  HttpRequest::HttpRequest (const String& rmth, 
			    const Uri& uri) : HttpProto (HTTP_CLIENT) {
    // reset protocol
    reset ();
    // set the request data
    d_rmth = rmth;
    d_ruri = http_uri_ruri (uri);
    // set the default header
    hset (HTTP_HOST_ATTR, uri.getauth ());
    hset (HTTP_CONN_ATTR, HTTP_CONN_XDEF);
  }

  HttpRequest::HttpRequest (InputStream& is) : HttpProto (HTTP_SERVER) {
    parse (is);
  }

  // copy construct this http request

  HttpRequest::HttpRequest (const HttpRequest& that) {
    that.rdlock ();
    try {
      HttpProto::operator = (that);
      d_rmth = that.d_rmth;
      d_ruri = that.d_ruri;
      d_rqry = that.d_rqry;
      that.unlock ();
    } catch (...) {
      that.unlock ();
      throw;
    }
  }

  // return the class name

  String HttpRequest::repr (void) const {
    return "HttpRequest";
  }

  // return a clone of this object

  Object* HttpRequest::clone (void) const {
    return new HttpRequest (*this);
  }

  // assign a http request to this one

  HttpRequest& HttpRequest::operator = (const HttpRequest& that) {
    // check for self assignation
    if (this == &that) return *this;
    // lock and assign
    wrlock ();
    that.rdlock ();
    try {
      HttpProto::operator = (that);
      d_rmth = that.d_rmth;
      d_ruri = that.d_ruri;
      d_rqry = that.d_rqry;
      unlock ();
      that.unlock ();
      return *this;
    } catch (...) {
      unlock ();
      that.unlock ();
      throw;
    }
  }

  // reset the request data and header

  void HttpRequest::reset (void) {
    wrlock ();
    try {
      HttpProto::reset ();
      d_rmth = HTTP_RMTH_XDEF;
      d_ruri = http_rmth_ruri (d_rmth);
      d_rqry = "";
      unlock ();
    } catch (...) {
      unlock ();
      throw;
    }
  }

  // parse the request input stream

  void HttpRequest::parse (InputStream& is) {
    wrlock ();
    try {
      // reset everything
      reset ();
      // parse the input stream
      Strvec rvec = http_parse_request (is);
      // get the request length
      long   rlen = rvec.length (); 
      // set the request value
      d_rmth = (rlen < 1) ? HTTP_RMTH_XDEF : rvec.get (0);
      d_ruri = (rlen < 2) ? http_rmth_ruri (d_rmth) : rvec.get (1);
      if (rlen == 3) {
	setvers (rvec.get (2));
      } else {
	d_vers = HTTP_VERS_NULL;
      }
      // read the header
      if (rlen == 3) hparse (is);
      // read the query string if any
      d_rqry = http_parse_query (d_rmth, d_ruri, d_head, is);
      // unlock and return
      unlock ();
    } catch (...) {
      unlock ();
      throw;
    }
  }
  
  // write the http request to an output stream
  
  void HttpRequest::write (OutputStream& os) const {
    rdlock ();
    try {
      // write the request
      String rmth = d_rmth + ' ' + d_ruri + ' ' + d_vers;
      os.writeln (rmth, true);
      // write the header
      HttpProto::write (os);
      // done
      unlock ();
    } catch (...) {
      unlock ();
      throw;
    }
  }
  
  // write the http request to a buffer
  
  void HttpRequest::write (Buffer& buf) const {
    rdlock ();
    try {
      // write the request
      String rmth = d_rmth + ' ' + d_ruri + ' ' + d_vers;
      buf.add (rmth);
      buf.add (crlq);
      buf.add (eolq);
      // write the header
      HttpProto::write (buf);
      // done
      unlock ();
    } catch (...) {
      unlock ();
      throw;
    }
  }

  // set the request method
  
  void HttpRequest::setrmth (const String& rmth) {
    wrlock ();
    try {
      d_rmth = rmth;
      unlock ();
    } catch (...) {
      unlock ();
      throw;
    }
  }
  
  // get the request method

  String HttpRequest::getrmth (void) const {
    rdlock ();
    try {
      String result = d_rmth;
      unlock ();
      return result;
    } catch (...) {
      unlock ();
      throw;
    }
  }
			       
  // set the request uri
  
  void HttpRequest::setruri (const String& ruri) {
    wrlock ();
    try {
      d_ruri = ruri;
      unlock ();
    } catch (...) {
      unlock ();
      throw;
    }
  }
  
  // get the request uri
  
  String HttpRequest::getruri (void) const {
    rdlock ();
    try {
      String result = d_ruri;
      unlock ();
      return result;
    } catch (...) {
      unlock ();
      throw;
    }
  }
  
  // get the request query string

  String HttpRequest::getrqry (void) const {
    rdlock ();
    try {
      String result = d_rqry;
      unlock ();
      return result;
    } catch (...) {
      unlock ();
      throw;
    }
  }

  // -------------------------------------------------------------------------
  // - object section                                                        -
  // -------------------------------------------------------------------------
  
  // the quark zone
  static const long QUARK_ZONE_LENGTH = 5;
  static QuarkZone  zone (QUARK_ZONE_LENGTH);
  
  // the object supported quarks
  static const long QUARK_SETRMTH = zone.intern ("set-method");
  static const long QUARK_GETRMTH = zone.intern ("get-method");
  static const long QUARK_SETRURI = zone.intern ("set-uri");
  static const long QUARK_GETRURI = zone.intern ("get-uri");
  static const long QUARK_GETRQRY = zone.intern ("get-query");
  
  // create a new object in a generic way
  
  Object* HttpRequest::mknew (Vector* argv) {
    long argc = (argv == nilp) ? 0 : argv->length ();
    // check for 0 argument
    if (argc == 0) return new HttpRequest;
    // check for 1 argument
    if (argc == 1) {
      // check for a string
      Object*  obj = argv->get (0);
      String* sobj = dynamic_cast <String*> (obj);
      if (sobj != nilp) return new HttpRequest (*sobj);
      // check for a uri
      Uri* uobj = dynamic_cast <Uri*> (obj);
      if (uobj != nilp) return new HttpRequest (*uobj);
      // check for an input stream
      InputStream* is = dynamic_cast <InputStream*> (obj);
      if (is != nilp) return new HttpRequest (*is);
      throw Exception ("type-error", 
		       "invalid object with http request constructor",
		       Object::repr (obj));
    }
    //check for 2 arguments
    if (argc == 2) {
      // get the command
      String rmth = argv->getstring (0);
      // check for a string
      Object*  obj = argv->get (1);
      String* sobj = dynamic_cast <String*> (obj);
      if (sobj != nilp) return new HttpRequest (rmth, *sobj);
      // check for a uri
      Uri* uobj = dynamic_cast <Uri*> (obj);
      if (uobj != nilp) return new HttpRequest (rmth, *uobj);
      throw Exception ("type-error", 
		       "invalid object with http request constructor",
		       Object::repr (obj));
    }
    // wrong arguments
    throw Exception ("argument-error", 
		     "too many arguments with http request constructor");
  }

  // return true if the given quark is defined
  
  bool HttpRequest::isquark (const long quark, const bool hflg) const {
    rdlock ();
    if (zone.exists (quark) == true) {
      unlock ();
      return true;
    }
    bool result = hflg ? HttpProto::isquark (quark, hflg) : false;
    unlock ();
    return result;
  }
  
  // apply this object with a set of arguments and a quark
  
  Object* HttpRequest::apply (Runnable* robj, Nameset* nset, const long quark,
			      Vector* argv) {
    // get the number of arguments
    long argc = (argv == nilp) ? 0 : argv->length ();
    
    // dispatch 0 argument
    if (argc == 0) {
      if (quark == QUARK_GETRMTH) return new String (getrmth ());
      if (quark == QUARK_GETRURI) return new String (getruri ());
      if (quark == QUARK_GETRQRY) return new String (getrqry ());
    }
    // dispatch 1 argument
    if (argc == 1) {
      if (quark == QUARK_SETRMTH) {
	String rmth = argv->getstring (0);
	setrmth (rmth);
	return nilp;
      }
      if (quark == QUARK_SETRURI) {
	String ruri = argv->getstring (0);
	setrmth (ruri);
	return nilp;
      }
    }
    // call the http proto method
    return HttpProto::apply (robj, nset, quark, argv);
  }
}
