// ---------------------------------------------------------------------------
// - HttpResponse.cpp                                                        -
// - afnix:nwg module - http response 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-2008 amaury darsch                                   -
// ---------------------------------------------------------------------------

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

namespace afnix {

  // -------------------------------------------------------------------------
  // - private section                                                       -
  // -------------------------------------------------------------------------
  
  // the http status code
  static const long HTTP_STAT_200 = 200; // OK
  static const long HTTP_STAT_201 = 201; // Created
  static const long HTTP_STAT_301 = 301; // Move Permanently
  static const long HTTP_STAT_302 = 302; // Found
  static const long HTTP_STAT_303 = 303; // See Other

  /// the status line regex
  static const String HTTP_RESP_RGX = "HTTP/1.$d$b+($d+)$b+$N+";
  /// the content length property
  static const String HTTP_HEAD_LEN = "Content-Length";
  /// the content type property
  static const String HTTP_HEAD_CTY = "Content-Type";
  /// the location property
  static const String HTTP_HEAD_LOC = "Location";
  /// the property regex
  static const String HTTP_PROP_RGX = "(<$a->+):$b+($N+)";
  /// the media type regex
  static const String HTTP_MIME_RGX = "(<$a-+>+/<$a-+>+)[;$N+]?";
  /// the charset regex
  static const String HTTP_CHAR_RGX = "$N+charset=(<$a->+)";
  /// the default http media type
  static const String HTTP_MIME_DEF = "text/html";
  /// the default http charset
  static const String HTTP_CHAR_DEF = "DEFAULT";
  /// the default content length
  static const long   HTTP_CLEN_DEF = -1;

  // this procedure returns the response status code
  static long http_status_code (Input* is) {
    // check for an input stream
    if (is == nilp){
      throw Exception ("htpp-error", 
		       "invalid nil input stream while reading status");
    }
    // check for valid stream
    if (is->valid () == false) {
      throw Exception ("http-error", "http status timeout");
    }
    // create a status regex
    Regex re = HTTP_RESP_RGX;
    // get the status line
    String line = is->readln ();
    if (re == line) {
      String code = re.getstr (0);
      return Utility::tointeger (code);
    }
    throw Exception ("http-error", "invalid http status line", line);
  }

  // this procedure read the http header and update the property list
  static void http_read_header (Plist& head, Input* is) {
    // do nothing without a stream
    if (is == nilp) return;
    // create a property regex
    Regex re = HTTP_PROP_RGX;
    // 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);
	head.set (name.strip (), pval.strip ());
      }
    }
  }

  // this procedure extract the media type from the header
  static String http_get_media (const Plist& head) {
    // check for the content type property
    if (head.exists (HTTP_HEAD_CTY) == true) {
      // get the content type property
      String pval = head.getpval (HTTP_HEAD_CTY);
      // extract the content value
      Regex re = HTTP_MIME_RGX;
      if (re == pval) return re.getstr (0);
    }
    // by default we operate in byte mode
    return HTTP_MIME_DEF;
  }

  // this procedure returns true if the encoding mode is defined in the header
  static bool http_is_emod (const Plist& head) {
    // check for the content type property
    if (head.exists (HTTP_HEAD_CTY) == true) {
      // get the content type property
      String pval = head.getpval (HTTP_HEAD_CTY);
      // extract the content value
      Regex re = HTTP_CHAR_RGX;
      return (re == pval);
    }
    // not defined
    return false;
  }

  // this procedure extract the character set from the header
  static String http_get_emod (const Plist& head) {
    // check for the content type property
    if (head.exists (HTTP_HEAD_CTY) == true) {
      // get the content type property
      String pval = head.getpval (HTTP_HEAD_CTY);
      // extract the content value
      Regex re = HTTP_CHAR_RGX;
      if (re == pval) return re.getstr (0);
    }
    // by default we operate in byte mode
    return HTTP_CHAR_DEF;
  }

  // return the header response content length
  static long http_get_length (const Plist& head) {
    // check for a content lenth property
    if (head.exists (HTTP_HEAD_LEN) == true) {
      // get the content length property
      String pval = head.getpval (HTTP_HEAD_LEN);
      // convert to an integer
      return Utility::tointeger(pval);
    }
    // no length specified
    return HTTP_CLEN_DEF;
  }

  // -------------------------------------------------------------------------
  // - class section                                                         -
  // -------------------------------------------------------------------------
  
  // create a default response

  HttpResponse::HttpResponse (void) {
    p_is = nilp;
    reset ();
  }
    
  // create a http response by stream
  
  HttpResponse::HttpResponse (Input* is){
    p_is = nilp;
    setis (is);
  }
  
  // destroy this http response

  HttpResponse::~HttpResponse (void) {
    Object::dref (p_is);
  }

  // return the class name

  String HttpResponse::repr (void) const {
    return "HttpResponse";
  }
  
  // reset the response and reset

  void HttpResponse::reset (void) {
    wrlock ();
    try {
      // reset the stream buffer
      d_sbuf.reset ();
      // reset the response header
      d_head.reset ();
      d_code = 0;
      // reset the local data
      d_clen = HTTP_CLEN_DEF;
      d_ccnt = 0;
      // unlock and return
      unlock ();
    } catch (...) {
      Object::dref (p_is);
      p_is = nilp;
      unlock ();
      throw;
    }
  }

  // set a new input stream and rescan

  void HttpResponse::setis (Input* is) {
    wrlock ();
    try {
      // reset everything
      reset ();
      // set the new stream
      Object::iref (is);
      Object::dref (p_is);
      p_is = is;
      // reset the encoding mode
      if (p_is != nilp) {
	settmod (p_is->gettmod ());
	setemod (p_is->getemod ());
      }
      // read the header status
      d_code = http_status_code (p_is);
      // read the header
      http_read_header (d_head, p_is);
      // update the character set
      setemod (http_get_emod (d_head));
      // update the content length
      d_clen = http_get_length (d_head);
      // here we are ready to read
      unlock ();
    } catch (...) {
      Object::dref (p_is);
      p_is = nilp;
      unlock ();
      throw;
    }
  }

  // get the header length

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

  // return true if a header property exists

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

  // get a header property by index
  
  Property* HttpResponse::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* HttpResponse::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* HttpResponse::hlookup (const String& name) const {
    rdlock ();
    try {
      Property* result = d_head.lookup (name);
      unlock ();
      return result;
    } catch (...) {
      unlock ();
      throw;
    }
  }

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

  // return the status code

  long HttpResponse::getcode (void) const {
    rdlock ();
    long result = d_code;
    unlock ();
    return result;
  }

  // return the media type


  String HttpResponse::getmedia (void) const {
    rdlock ();
    try {
      String result = http_get_media (d_head);
      unlock ();
      return result;
    } catch (...) {
      unlock ();
      throw;
    }
  }

  // return true if the status code is ok

  bool HttpResponse::isok (void) const {
    rdlock ();
    bool status = (d_code == HTTP_STAT_200);
    unlock ();
    return status;
  }

  // return true if the encoding mode is defined in the header

  bool HttpResponse::isemod (void) const {
    rdlock ();
    try {
      bool result = http_is_emod (d_head);
      unlock ();
      return result;
    } catch (...) {
      unlock ();
      throw;
    }
  }

  // return true if there is something at another location

  bool HttpResponse::ishloc (void) const {
    rdlock ();
    try {
      // check first for a location property
      if (hexists (HTTP_HEAD_LOC) == false) {
	unlock ();
	return false;
      }
      // check the status code
      bool status = false;
      switch (d_code) {
      case HTTP_STAT_201:
      case HTTP_STAT_301:
      case HTTP_STAT_302:
      case HTTP_STAT_303:
	status = true;
	break;
      default:
	status = false;
	break;
      }
      unlock ();
      return status;
    } catch (...) {
      unlock ();
      throw;
    }
  }

  // return the header location value

  String HttpResponse::gethloc (void) const {
    rdlock ();
    try {
      String result = hgetval (HTTP_HEAD_LOC);
      unlock ();
      return result;
    } catch (...) {
      unlock ();
      throw;
    }
  }

  // return the stream descriptor

  int HttpResponse::getsid (void) const {
    rdlock ();
    try {
      int result = (p_is == nilp) ? Input::getsid () : p_is->getsid ();
      unlock ();
      return result;
    } catch (...) {
      Object::dref (p_is);
      unlock ();
      throw;
    }	 
  }

  // return the next available character

  char HttpResponse::read (void) {
    wrlock ();
    try {
      // check for content length
      if ((d_clen >= 0) && (d_ccnt >= d_clen)) {
	unlock ();
	return eosc;
      }
      // check the local pushback buffer
      if (d_sbuf.empty () == false) {
	char result = d_sbuf.read ();
	d_ccnt++;
	unlock ();
	return result;
      }
      // check for nil stream
      if (p_is == nilp) {
	unlock ();
	return eosc;
      }
      // read the base stream
      char result = p_is->read ();
      d_ccnt++;
      unlock ();
      return result;
    } catch (...) {
      unlock ();
      throw;
    }
  }

  // return true if we are at the eos

  bool HttpResponse::iseos (void) const {
    rdlock ();
    try {
      // check for content length
      if ((d_clen >= 0) && (d_ccnt >= d_clen)) {
	unlock ();
	return true;
      }
      // check the local pushback buffer
      if (d_sbuf.empty () == false) {
	unlock ();
	return false;
      }
      // check for nil stream
      if (p_is == nilp) {
	unlock ();
	return true;
      }
      // read the stream status
      bool result = p_is->iseos (); 
      unlock ();
      return result;
    } catch (...) {
      unlock ();
      throw;
    }
  }

  // check if we can read a character

  bool HttpResponse::valid (void) const {
    rdlock ();
    try {
      // check for content length
      if ((d_clen >= 0) && (d_ccnt >= d_clen)) {
	unlock ();
	return false;
      }
      // check the local pushback buffer
      if (d_sbuf.empty () == false) {
	unlock ();
	return true;
      }
      // check for nil stream
      if (p_is == nilp) {
	unlock ();
	return false;
      }
      // read the stream status
      bool result = p_is->valid (); 
      unlock ();
      return result;
    } catch (...) {
      unlock ();
      throw;
    }
  }

  // pushback a character

  void HttpResponse::pushback (const char value) {
    wrlock ();
    try {
      if (p_is == nilp) {
	long blen = d_sbuf.length ();
	Input::pushback (value);
	d_ccnt -= (d_sbuf.length () - blen);
      } else {
	long blen = p_is->buflen ();
	p_is->pushback (value);
	d_ccnt -= (p_is->buflen () - blen);
      }
      if (d_ccnt < 0) d_ccnt = 0;
      unlock ();
    } catch (...) {
      unlock ();
      throw;
    }
  }

  // pushback a c-string on this stream

  void HttpResponse::pushback (const char* s) {
    wrlock ();
    try {
      if (p_is == nilp) {
	long blen = d_sbuf.length ();
	Input::pushback (s);
	d_ccnt -= (d_sbuf.length () - blen);
      } else {
	long blen = p_is->buflen ();
	p_is->pushback (s);
	d_ccnt -= (p_is->buflen () - blen);
      }
      if (d_ccnt < 0) d_ccnt = 0;
      unlock ();
    } catch (...) {
      unlock ();
      throw;
    }
  }

  // pushback a c-string on this stream

  void HttpResponse::pushback (const char* s, const long size) {
    wrlock ();
    try {
      if (p_is == nilp) {
	long blen = d_sbuf.length ();
	Input::pushback (s, size);
	d_ccnt -= (d_sbuf.length () - blen);
      } else {
	long blen = p_is->buflen ();
	p_is->pushback (s, size);
	d_ccnt -= (p_is->buflen () - blen);
      }
      if (d_ccnt < 0) d_ccnt = 0;
      unlock ();
    } catch (...) {
      unlock ();
      throw;
    }
  }

  // pushback a string on this stream
  
  void HttpResponse::pushback (const String& s) {
    wrlock ();
    try {
      if (p_is == nilp) {
	long blen = d_sbuf.length ();
	Input::pushback (s);
	d_ccnt -= (d_sbuf.length () - blen);
      } else {
	long blen = p_is->buflen ();
	p_is->pushback (s);
	d_ccnt -= (p_is->buflen () - blen);
      }
      if (d_ccnt < 0) d_ccnt = 0;
      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_OKP      = zone.intern ("ok-p");
  static const long QUARK_EMODP    = zone.intern ("encoding-mode-p");
  static const long QUARK_LOCP     = zone.intern ("location-p");
  static const long QUARK_HGET     = zone.intern ("header-get");
  static const long QUARK_HFIND    = zone.intern ("header-find");
  static const long QUARK_RESET    = zone.intern ("reset");
  static const long QUARK_SETIS    = zone.intern ("set-input-stream");
  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");  
  static const long QUARK_HGETVAL  = zone.intern ("header-get-value");  
  static const long QUARK_GETCODE  = zone.intern ("get-status-code");
  static const long QUARK_GETHLOC  = zone.intern ("get-location");
  static const long QUARK_GETMEDIA = zone.intern ("get-media-type");
  
  // create a new object in a generic way
  
  Object* HttpResponse::mknew (Vector* argv) {
    long argc = (argv == nilp) ? 0 : argv->length ();
    // check for 0 argument
    if (argc == 0) return new HttpResponse;
    // check for 1 argument
    if (argc == 1) {
      Object* obj = argv->get (0);
      Input* is = dynamic_cast <Input*> (obj);
      if (is == nilp) {
	throw Exception ("type-error", 
			 "invalid object for http response constructor",
			 Object::repr (obj));
      }
      return new HttpResponse (is);
    }
    throw Exception ("argument-error", 
		     "too many arguments with http response constructor");
  }

  // return true if the given quark is defined
  
  bool HttpResponse::isquark (const long quark, const bool hflg) const {
    rdlock ();
    if (zone.exists (quark) == true) {
      unlock ();
      return true;
    }
    bool result = hflg ? Input::isquark (quark, hflg) : false;
    unlock ();
    return result;
  }
  
  // apply this object with a set of arguments and a quark
  
  Object* HttpResponse::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_OKP)      return new Boolean (isok     ());
      if (quark == QUARK_EMODP)    return new Boolean (isemod   ());
      if (quark == QUARK_LOCP)     return new Boolean (ishloc   ());
      if (quark == QUARK_HLENGTH)  return new Integer (hlength  ());
      if (quark == QUARK_GETCODE)  return new Integer (getcode  ());
      if (quark == QUARK_GETHLOC)  return new String  (gethloc  ());
      if (quark == QUARK_GETMEDIA) return new String  (getmedia ());
      if (quark == QUARK_RESET) {
	reset ();
	return nilp;
      }
    }
    // dispatch 1 argument
    if (argc == 1) {
      if (quark == QUARK_SETIS) {
	Object* obj = argv->get (0);
	Input* is = dynamic_cast <Input*> (obj);
	if (is == nilp) {
	  throw Exception ("type-error", 
			   "invalid http response input stream object",
			   Object::repr (obj));
	}
	setis (is);
	return nilp;
      }
      if (quark == QUARK_HEXISTP) {
        String name = argv->getstring (0);
        return new Boolean (hexists (name));
      }
      if (quark == QUARK_HGETVAL) {
        String name = argv->getstring (0);
        return new String (hgetval (name));
      }
      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_HGET) {
        long index = argv->getint (0);
        rdlock();
        try {
          Object* result = hget (index);
          robj->post (result);
          unlock ();
          return result;
        } catch (...) {
          unlock ();
          throw;
        }
      }

    }
    // call the input method
    return Input::apply (robj, nset, quark, argv);
  }
}
