// ---------------------------------------------------------------------------
// - HttppProto.cpp                                                          -
// - afnix:nwg module - http protocol 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-2009 amaury darsch                                   -
// ---------------------------------------------------------------------------

#include "Regex.hpp"
#include "Vector.hpp"
#include "Integer.hpp"
#include "Boolean.hpp"
#include "Utility.hpp"
#include "Runnable.hpp"
#include "HttpProto.hpp"
#include "QuarkZone.hpp"
#include "Exception.hpp"

namespace afnix {

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

  // the compatible http version
  static const String HTTP_VERS_CPT0 = "HTTP/1.0";
  // the current http version
  static const String HTTP_VERS_CPT1 = "HTTP/1.1";
  // the default http version
  static const String HTTP_VERS_XDEF = HTTP_VERS_CPT0;

  /// the property regex
  static const String HTTP_PROP_RGEX = "(<$a->+):$b+($N+)";

  // the http status code
  static const String HTTP_STAT_C100 = "100 Continue";
  static const String HTTP_STAT_C101 = "101 Switching Protocols";
  static const String HTTP_STAT_C200 = "200 OK";
  static const String HTTP_STAT_C201 = "201 Created";
  static const String HTTP_STAT_C202 = "202 Accepted";
  static const String HTTP_STAT_C203 = "203 Non-Authoritative Information";
  static const String HTTP_STAT_C204 = "204 No Content";
  static const String HTTP_STAT_C205 = "205 Reset Content";
  static const String HTTP_STAT_C206 = "206 Partial Content";
  static const String HTTP_STAT_C300 = "300 Multiple Choices";
  static const String HTTP_STAT_C301 = "301 Moved Permanently";
  static const String HTTP_STAT_C302 = "302 Found";
  static const String HTTP_STAT_C303 = "303 See Other";
  static const String HTTP_STAT_C304 = "304 Not Modified";
  static const String HTTP_STAT_C305 = "305 Use Proxy";
  static const String HTTP_STAT_C307 = "307 Temporary Redirect";
  static const String HTTP_STAT_C400 = "400 Bad Request";
  static const String HTTP_STAT_C401 = "401 Unauthorized";
  static const String HTTP_STAT_C402 = "402 Payment Required";
  static const String HTTP_STAT_C403 = "403 Forbidden";
  static const String HTTP_STAT_C404 = "404 Not Found";
  static const String HTTP_STAT_C405 = "405 Method Not Allowed";
  static const String HTTP_STAT_C406 = "406 Not Acceptable";
  static const String HTTP_STAT_C407 = "407 Proxy Authentication Required";
  static const String HTTP_STAT_C408 = "408 Request Time-out";
  static const String HTTP_STAT_C409 = "409 Conflict";
  static const String HTTP_STAT_C410 = "410 Gone";
  static const String HTTP_STAT_C411 = "411 Length Required";
  static const String HTTP_STAT_C412 = "412 Precondition Failed";
  static const String HTTP_STAT_C413 = "413 Request Entity Too Large";
  static const String HTTP_STAT_C414 = "414 Request-URI Too Large";
  static const String HTTP_STAT_C415 = "415 Unsupported Media Type";
  static const String HTTP_STAT_C416 = "416 Requested range not satisfiable";
  static const String HTTP_STAT_C417 = "417 Expectation Failed";
  static const String HTTP_STAT_C500 = "500 Internal Server Error";
  static const String HTTP_STAT_C501 = "501 Not Implemented";
  static const String HTTP_STAT_C502 = "502 Bad Gateway";
  static const String HTTP_STAT_C503 = "503 Service Unavailable";
  static const String HTTP_STAT_C504 = "504 Gateway Time-out";
  static const String HTTP_STAT_C505 = "505 HTTP Version not supported";


  // this procedure format a property into a string header
  static String get_header_string (const Property& prop) {
    // get the property name
    String result = prop.getname ();
    // add separator
    result += ": ";
    // add value
    result += prop.getpval ();
    // here we are
    return result;
  }

  // -------------------------------------------------------------------------
  // - public section                                                         -
  // -------------------------------------------------------------------------

  // map a http status code to a message

  String HttpProto::mapcode (const long code) {
    switch (code) {
    case 100: return HTTP_STAT_C100;
    case 101: return HTTP_STAT_C101;
    case 200: return HTTP_STAT_C200;
    case 201: return HTTP_STAT_C201;
    case 202: return HTTP_STAT_C202;
    case 203: return HTTP_STAT_C203;
    case 204: return HTTP_STAT_C204;
    case 205: return HTTP_STAT_C205;
    case 206: return HTTP_STAT_C206;
    case 300: return HTTP_STAT_C300;
    case 301: return HTTP_STAT_C301;
    case 302: return HTTP_STAT_C302;
    case 303: return HTTP_STAT_C303;
    case 304: return HTTP_STAT_C304;
    case 305: return HTTP_STAT_C305;
    case 307: return HTTP_STAT_C307;
    case 400: return HTTP_STAT_C400;
    case 401: return HTTP_STAT_C401;
    case 402: return HTTP_STAT_C402;
    case 403: return HTTP_STAT_C403;
    case 404: return HTTP_STAT_C404;
    case 405: return HTTP_STAT_C405;
    case 406: return HTTP_STAT_C406;
    case 407: return HTTP_STAT_C407;
    case 408: return HTTP_STAT_C408;
    case 409: return HTTP_STAT_C409;
    case 410: return HTTP_STAT_C410;
    case 411: return HTTP_STAT_C411;
    case 412: return HTTP_STAT_C412;
    case 413: return HTTP_STAT_C413;
    case 414: return HTTP_STAT_C414;
    case 415: return HTTP_STAT_C415;
    case 416: return HTTP_STAT_C416;
    case 417: return HTTP_STAT_C417;
    case 500: return HTTP_STAT_C500;
    case 501: return HTTP_STAT_C501;
    case 502: return HTTP_STAT_C502;
    case 503: return HTTP_STAT_C503;
    case 504: return HTTP_STAT_C504;
    case 505: return HTTP_STAT_C505;
    default:
      break;
    }
    throw Exception ("http-error", "invalid http status code", 
		     Utility::tostring (code));
  }

  // -------------------------------------------------------------------------
  // - class section                                                         -
  // -------------------------------------------------------------------------

  // create a default http proto

  HttpProto::HttpProto (void) {
    reset ();
  }

  // reset this protocol version

  void HttpProto::reset (void) {
    wrlock ();
    try {
      d_vers = HTTP_VERS_XDEF;
      d_head.reset ();
      unlock ();
    } catch (...) {
      unlock ();
      throw;
    }
  }

  // set the http protocol version

  void HttpProto::setvers (const String& vers) {
    wrlock ();
    try {
      if ((vers != HTTP_VERS_CPT0) && (vers != HTTP_VERS_CPT1)) {
	throw Exception ("http-error", "invalid protocol version", vers);
      }
      d_vers = vers;
      unlock ();
    } catch (...) {
      unlock ();
      throw;
    }
  }

  // get the http protocol version

  String HttpProto::getvers (void) const {
    rdlock ();
    try {
      String result = d_vers;
      unlock ();
      return result;
    } catch (...) {
      unlock ();
      throw;
    }
  }

  // get the header length

  long HttpProto::hlength (void) const {
    rdlock ();
    try {
      long result = d_head.length ();
      unlock ();
      return result;
    } catch (...) {
      unlock ();
      throw;
    }
  }

  // return true if a header property exists

  bool HttpProto::hexists (const String& name) const {
    rdlock ();
    try {
      bool result = d_head.exists (name);
      unlock ();
      return result;
    } catch (...) {
      unlock ();
      throw;
    }
  }

  // set a header property by name and literal

  void HttpProto::hset (const String& name, const Literal& lval) {
    wrlock ();
    try {
      d_head.set (name, lval.tostring ());
      unlock ();
    } catch (...) {
      unlock ();
      throw;
    }
  }

  // get a header property by index
  
  Property* HttpProto::hget (const long index) const {
    rdlock ();
    try {
      Property* result = d_head.get (index);
      unlock ();
      return result;
    } catch (...) {
      unlock ();
      throw;
    }
  }

  // get a header property by name
  
  Property* HttpProto::hfind (const String& name) const {
    rdlock ();
    try {
      Property* result = d_head.find (name);
      unlock ();
      return result;
    } catch (...) {
      unlock ();
      throw;
    }
  }

  // get a header property by name or throw an exception
  
  Property* HttpProto::hlookup (const String& name) const {
    rdlock ();
    try {
      Property* result = d_head.lookup (name);
      unlock ();
      return result;
    } catch (...) {
      unlock ();
      throw;
    }
  }

  // map a header property value by name
  
  String HttpProto::hmap (const String& name) const {
    rdlock ();
    try {
      String result = d_head.getpval (name);
      unlock ();
      return result;
    } catch (...) {
      unlock ();
      throw;
    }
  }

  // parse a header from an input stream

  void HttpProto::hparse (InputStream& is) {
    wrlock ();
    try {
      // create a property regex
      Regex re = HTTP_PROP_RGEX;
      // loop while valid
      while (is.valid () == true) {
	// get the header line
	String line = is.readln ();
	// check for last empty line
	if (line.isnil () == true) break;
	// check for valid header
	if (re == line) {
	  String name = re.getstr (0);
	  String pval = re.getstr (1);
	  d_head.set (name.strip (), pval.strip ());
	} else {
	  throw Exception ("http-error", "invalid header content", line);
	}
      }
      unlock ();
    } catch (...) {
      unlock ();
      throw;
    }
  }

  // write the http header to the output stream

  void HttpProto:: write (OutputStream& os) const {
    rdlock ();
    try {
      long hlen = d_head.length ();
      for (long i = 0; i < hlen; i++) {
	// get the property
	Property* prop = d_head.get (i);
	if (prop == nilp) continue;
	// format the string and write
	os.writeln (get_header_string (*prop), true);
      }
      os.newline (true);
      // done
      unlock ();
    } catch (...) {
      unlock ();
      throw;
    }
  }

  // write the http reply to a buffer

  void HttpProto:: write (Buffer& buf) const {
    rdlock ();
    try {
      long hlen = d_head.length ();
      for (long i = 0; i < hlen; i++) {
	// get the property
	Property* prop = d_head.get (i);
	if (prop == nilp) continue;
	// format the string and write
	buf.add (get_header_string (*prop));
	buf.add (crlc);
	buf.add (eolc);
      }
      buf.add (crlc);
      buf.add (eolc);
      // done
      unlock ();
    } catch (...) {
      unlock ();
      throw;
    }
  }

  // -------------------------------------------------------------------------
  // - object section                                                        -
  // -------------------------------------------------------------------------

  // the quark zone
  static const long QUARK_ZONE_LENGTH = 13;
  static QuarkZone  zone (QUARK_ZONE_LENGTH);

  // the object supported quarks
  static const long QUARK_HSET    = zone.intern ("header-set");
  static const long QUARK_HGET    = zone.intern ("header-get");
  static const long QUARK_HMAP    = zone.intern ("header-map");  
  static const long QUARK_RESET   = zone.intern ("reset");
  static const long QUARK_PARSE   = zone.intern ("parse");
  static const long QUARK_WRITE   = zone.intern ("write");
  static const long QUARK_HFIND   = zone.intern ("header-find");
  static const long QUARK_SETVERS = zone.intern ("set-version");  
  static const long QUARK_GETVERS = zone.intern ("get-version");  
  static const long QUARK_HLENGTH = zone.intern ("header-length");
  static const long QUARK_HEXISTP = zone.intern ("header-exists-p");
  static const long QUARK_HLOOKUP = zone.intern ("header-lookup");  

  // return true if the given quark is defined

  bool HttpProto::isquark (const long quark, const bool hflg) const {
    rdlock ();
    if (zone.exists (quark) == true) {
      unlock ();
      return true;
    }
    bool result = hflg ? Object::isquark (quark, hflg) : false;
    unlock ();
    return result;
  }

  // apply this object with a set of arguments and a quark

  Object* HttpProto::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_GETVERS) return new String  (getvers ());
      if (quark == QUARK_HLENGTH) return new Integer (hlength ());
      if (quark == QUARK_RESET) {
	reset ();
	return nilp;
      }
      if (quark == QUARK_WRITE) {
	OutputStream* os = (robj == nilp) ? nilp : robj->getos ();
	if (os == nilp) return nilp;
	write (*os);
	return nilp;
      }
    }

    // dispatch 1 argument
    if (argc == 1) {
      if (quark == QUARK_SETVERS) {
        String vers = argv->getstring (0);
	setvers (vers);
	return nilp;
      }
      if (quark == QUARK_PARSE) {
	Object* obj = argv->get (0);
	InputStream* is = dynamic_cast <InputStream*> (obj);
	if (is == nilp) {
	  throw Exception ("type-error", 
			   "invalid object as an input stream to parse",
			   Object::repr (obj));
	}
	parse (*is);
	return nilp;
      }
      if (quark == QUARK_HEXISTP) {
        String name = argv->getstring (0);
        return new Boolean (hexists (name));
      }
      if (quark == QUARK_HMAP) {
        String name = argv->getstring (0);
        return new String (hmap (name));
      }
      if (quark == QUARK_HGET) {
        long index = argv->getint (0);
        rdlock();
        try {
          Object* result = hget (index);
          robj->post (result);
          unlock ();
          return result;
        } catch (...) {
          unlock ();
          throw;
        }
      }
      if (quark == QUARK_HFIND) {
        rdlock ();
        try {
          String name = argv->getstring (0);
          Object* result = hfind (name);
          robj->post (result);
          unlock ();
          return result;
        } catch (...) {
          unlock ();
          throw;
        }
      }
      if (quark == QUARK_HLOOKUP) {
        rdlock ();
        try {
          String name = argv->getstring (0);
          Object* result = hlookup (name);
          robj->post (result);
          unlock ();
          return result;
        } catch (...) {
          unlock ();
          throw;
        }
      }
      if (quark == QUARK_WRITE) {
	Object* obj = argv->get (0);
	// check for an output stream
	OutputStream* os = dynamic_cast <OutputStream*> (obj);
	if (os != nilp) {
	  write (*os);
	  return nilp;
	}
	// check for a buffer
	Buffer* buf = dynamic_cast <Buffer*> (obj);
	if (buf != nilp) {
	  write (*buf);
	  return nilp;
	}
	throw Exception ("type-error", "invalid object with write",
			 Object::repr (obj));
      }
    }
    // dispatch 2 argument
    if (argc == 2) {
      if (quark == QUARK_HSET) {
	String   name = argv->getstring (0);
        Object*   obj = argv->get (1);
        Literal* lobj = dynamic_cast <Literal*> (obj);
        if (lobj == nilp) {
          throw Exception ("type-error", "invalid object with set-header",
                           Object::repr (obj));
        }
	hset (name, *lobj);
	return nilp;
      }
    }
    // call the object method
    return Object::apply (robj, nset, quark, argv);
  }
}
