// Copyright (C) 2006  Davis E. King (davisking@users.sourceforge.net)
// License: Boost Software License   See LICENSE.txt for the full license.
#ifndef DLIB_SERVER_HTTp_1_
#define DLIB_SERVER_HTTp_1_


#include "server_iostream_abstract.h"
#include "server_http_abstract.h"
#include <iostream>
#include <sstream>
#include <string>
#include "../logger.h"

#ifdef  __INTEL_COMPILER
// ignore the bogus warning about hiding on_connect()
#pragma warning (disable: 1125)
#endif

namespace dlib
{

    template <
        typename server_base,
        typename map_ss_type,
        typename queue_string_type
        >
    class server_http_1 : public server_base 
    {

        /*!
            CONVENTION
                this extension doesn't add any new state to this object.
        !*/


    public:
        typedef map_ss_type map_type;
        typedef queue_string_type queue_type;

    private:

        virtual void on_request (
            const std::string& path,
            std::string& result,
            const map_type& queries,
            const map_type& cookies,
            queue_type& new_cookies,
            const map_type& incoming_headers,
            map_type& response_headers,
            const std::string& foreign_ip,
            const std::string& local_ip,
            unsigned short foreign_port,
            unsigned short local_port
        ) = 0;

        unsigned char to_hex (
            unsigned char ch
        ) const
        {
            if (ch <= '9' && ch >= '0')
                ch -= '0';
            else if (ch <= 'f' && ch >= 'a')
                ch -= 'a' - 10;
            else if (ch <= 'F' && ch >= 'A')
                ch -= 'A' - 10;
            else 
                ch = 0;
            return ch;
        }

        const std::string decode_query_string (
            const std::string& str
        ) const
        {
            using namespace std;
            string result;
            string::size_type i;
            for (i = 0; i < str.size(); ++i)
            {
                if (str[i] == '+')
                {
                    result += ' ';
                }
                else if (str[i] == '%' && str.size() > i+2)
                {
                    const unsigned char ch1 = to_hex(str[i+1]);
                    const unsigned char ch2 = to_hex(str[i+2]);
                    const unsigned char ch = (ch1 << 4) | ch2;
                    result += ch;
                    i += 2;
                }
                else
                {
                    result += str[i];
                }
            }
            return result;
        }

        void on_connect (
            std::istream& in,
            std::ostream& out,
            const std::string& foreign_ip,
            const std::string& local_ip,
            unsigned short foreign_port,
            unsigned short local_port,
            uint64
        )
        {
            bool my_fault = true;
            try
            {
                enum req_type {get, post} rtype;

                using namespace std;
                map_type cookies;
                string word;
                string path;
                in >> word;
                if (word == "GET" || word == "get")
                {
                    rtype = get;
                }
                else if ( word == "POST" || word == "post")
                {
                    rtype = post;
                }
                else
                {
                    // this isn't a GET or POST request so just drop the connection
                    return;
                }

                // get the path
                in >> path;

                // now loop over all the incoming_headers
                string line;
                getline(in,line);
                unsigned long content_length = 0;
                string content_type;
                map_type incoming_headers;
                string first_part_of_header;
                string::size_type position_of_double_point;
                while (line.size() > 2)
                {
                    position_of_double_point = line.find_first_of(':');
                    if ( position_of_double_point != string::npos )
                    {
                        first_part_of_header = line.substr(0, position_of_double_point);
                        if ( incoming_headers.is_in_domain(first_part_of_header) )
                        {
                            incoming_headers[ first_part_of_header ] += " " + line.substr(position_of_double_point+1);
                        }
                        else
                        {
                            string second_part_of_header(line.substr(position_of_double_point+1));
                            incoming_headers.add( first_part_of_header, second_part_of_header );
                        }

                        // look for Content-Type:
                        if (line.size() > 14 &&
                            line[0] == 'C' &&
                            line[1] == 'o' &&
                            line[2] == 'n' &&
                            line[3] == 't' &&
                            line[4] == 'e' &&
                            line[5] == 'n' &&
                            line[6] == 't' &&
                            line[7] == '-' &&
                            (line[8] == 'T' || line[8] == 't') &&
                            line[9] == 'y' &&
                            line[10] == 'p' &&
                            line[11] == 'e' &&
                            line[12] == ':' 
                        )
                        {
                            content_type = line.substr(14);
                            if (content_type[content_type.size()-1] == '\r')
                                content_type.erase(content_type.size()-1);
                        }
                        // look for Content-Length:
                        else if (line.size() > 16 &&
                                 line[0] == 'C' &&
                                 line[1] == 'o' &&
                                 line[2] == 'n' &&
                                 line[3] == 't' &&
                                 line[4] == 'e' &&
                                 line[5] == 'n' &&
                                 line[6] == 't' &&
                                 line[7] == '-' &&
                                 (line[8] == 'L' || line[8] == 'l') &&
                                 line[9] == 'e' &&
                                 line[10] == 'n' &&
                                 line[11] == 'g' &&
                                 line[12] == 't' &&
                                 line[13] == 'h' &&
                                 line[14] == ':' 
                        )
                        {
                            istringstream sin(line.substr(16));
                            sin >> content_length;
                            if (!sin)
                                content_length = 0;
                        }
                        // look for any cookies
                        else if (line.size() > 6 &&
                                 line[0] == 'C' &&
                                 line[1] == 'o' &&
                                 line[2] == 'o' &&
                                 line[3] == 'k' &&
                                 line[4] == 'i' &&
                                 line[5] == 'e' &&
                                 line[6] == ':' 
                        )
                        {
                            string::size_type pos = 6;
                            string key, value;
                            bool seen_key_start = false;
                            bool seen_equal_sign = false;
                            while (pos + 1 < line.size())
                            {
                                ++pos;
                                // ignore whitespace between cookies
                                if (!seen_key_start && line[pos] == ' ')
                                    continue;

                                seen_key_start = true;
                                if (!seen_equal_sign) 
                                {
                                    if (line[pos] == '=')
                                    {
                                        seen_equal_sign = true;
                                    }
                                    else
                                    {
                                        key += line[pos];
                                    }
                                }
                                else
                                {
                                    if (line[pos] == ';')
                                    {
                                        if (cookies.is_in_domain(key) == false)
                                            cookies.add(key,value);
                                        seen_equal_sign = false;
                                        seen_key_start = false;
                                    }
                                    else
                                    {
                                        value += line[pos];
                                    }
                                }
                            }
                            if (key.size() > 0 && cookies.is_in_domain(key) == false)
                                cookies.add(key,value);
                        }
                    } // no ':' in it!
                    getline(in,line);
                } // while (line.size() > 2 )

                // If there is data being posted back to us as a query string then
                // just stick it onto the end of the path so the following code can
                // then just pick it out like we do for GET requests.
                if (rtype == post && content_type == "application/x-www-form-urlencoded" 
                    && content_length > 0)
                {
                    line.resize(content_length);
                    in.read(&line[0],content_length);
                    path += "?" + line;
                }

                string result;
                map_type queries;
                string::size_type pos = path.find_first_of("?");
                if (pos != string::npos)
                {
                    word = path.substr(pos+1);
                    path = path.substr(0,pos);
                    for (pos = 0; pos < word.size(); ++pos)
                    {
                        if (word[pos] == '&')
                            word[pos] = ' ';
                    }

                    istringstream sin(word);
                    sin >> word;
                    while (sin)
                    {
                        pos = word.find_first_of("=");
                        if (pos != string::npos)
                        {
                            string key = decode_query_string(word.substr(0,pos));
                            string value = decode_query_string(word.substr(pos+1));
                            if (queries.is_in_domain(key) == false)
                                queries.add(key,value);
                        }
                        sin >> word;
                    }
                }


                my_fault = false;
                queue_type new_cookies;
                map_type response_headers;
                // if there wasn't a problem with the input stream at some point
                // then lets trigger this request callback.
                if (in)
                    on_request(path,result,queries,cookies,new_cookies,incoming_headers, response_headers, foreign_ip,local_ip,foreign_port,local_port);
                my_fault = true;

                out << "HTTP/1.0 200 OK\r\n";
                // only send this header if the user hasn't told us to send another kind
                if (response_headers.is_in_domain("Content-Type") == false && 
                    response_headers.is_in_domain("content-type") == false)
                {
                    out << "Content-Type: text/html\r\n";
                }
                out << "Content-Length: " << result.size() << "\r\n";

                // Set any new headers
                response_headers.reset();
                while (response_headers.move_next())
                    out << response_headers.element().key() << ':' << response_headers.element().value() << "\r\n";

                // set any cookies 
                new_cookies.reset();
                while (new_cookies.move_next())
                {
                    out << "Set-Cookie: " << new_cookies.element() << "\r\n";
                }
                out << "\r\n" << result;
            }
            catch (std::bad_alloc&)
            {
                dlog << LERROR << "We ran out of memory in server_http::on_connect()";
                // If this is an escaped exception from on_request then let it fly! 
                // Seriously though, this way it is obvious to the user that something bad happened
                // since they probably won't have the dlib logger enabled.
                if (!my_fault)
                    throw;
            }

        }

        const static logger dlog;
    };

    template <
        typename server_base,
        typename map_ss_type,
        typename queue_string_type
        >
    const logger server_http_1<server_base,map_ss_type,queue_string_type>::dlog("dlib.server");
}

#endif // DLIB_SERVER_HTTp_1_