// Copyright (C) 2003  Davis E. King (davis@dlib.net)
// License: Boost Software License   See LICENSE.txt for the full license.
#ifndef DLIB_SERVER_HTTP_CPp_
#define DLIB_SERVER_HTTP_CPp_

#include "server_http.h"

namespace dlib
{

// ----------------------------------------------------------------------------------------

    namespace http_impl
    {
        inline unsigned char to_hex( unsigned char x )  
        {
            return x + (x > 9 ? ('A'-10) : '0');
        }

        const std::string urlencode( const std::string& s )  
        {
            std::ostringstream os;

            for ( std::string::const_iterator ci = s.begin(); ci != s.end(); ++ci )
            {
                if ( (*ci >= 'a' && *ci <= 'z') ||
                     (*ci >= 'A' && *ci <= 'Z') ||
                     (*ci >= '0' && *ci <= '9') )
                { // allowed
                    os << *ci;
                }
                else if ( *ci == ' ')
                {
                    os << '+';
                }
                else
                {
                    os << '%' << to_hex(*ci >> 4) << to_hex(*ci % 16);
                }
            }

            return os.str();
        }

        inline unsigned char from_hex (
            unsigned char ch
        ) 
        {
            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 urldecode (
            const std::string& str
        ) 
        {
            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 = from_hex(str[i+1]);
                    const unsigned char ch2 = from_hex(str[i+2]);
                    const unsigned char ch = (ch1 << 4) | ch2;
                    result += ch;
                    i += 2;
                }
                else
                {
                    result += str[i];
                }
            }
            return result;
        }

        void parse_url(
            std::string word, 
            key_value_map& queries
        )
        /*!
            Parses the query string of a URL.  word should be the stuff that comes
            after the ? in the query URL.
        !*/
        {
            std::string::size_type pos;

            for (pos = 0; pos < word.size(); ++pos)
            {
                if (word[pos] == '&')
                    word[pos] = ' ';
            }

            std::istringstream sin(word);
            sin >> word;
            while (sin)
            {
                pos = word.find_first_of("=");
                if (pos != std::string::npos)
                {
                    std::string key = urldecode(word.substr(0,pos));
                    std::string value = urldecode(word.substr(pos+1));

                    queries[key] = value;
                }
                sin >> word;
            }
        }
      
        void read_with_limit(
            std::istream& in, 
            std::string& buffer, 
            int delim = '\n'
        ) 
        {
            using namespace std;
            const size_t max = 16*1024;
            buffer.clear();
            buffer.reserve(300);

            while (in.peek() != delim && in.peek() != EOF && buffer.size() < max)
            {
                buffer += (char)in.get();
            }

            // if we quit the loop because the data is longer than expected or we hit EOF
            if (in.peek() == EOF || buffer.size() == max)
                throw http_parse_error("HTTP field from client is too long", 414);

            // Make sure the last char is the delim.
            if (in.get() != delim) 
            {
                in.setstate(ios::badbit);
                buffer.clear();
            } 
            else 
            {
                // Read the remaining delimiters
                if (delim == ' ') 
                {
                    while (in.peek() == ' ')
                        in.get();
                }
            }
        }
    }

// ----------------------------------------------------------------------------------------

    unsigned long parse_http_request ( 
        std::istream& in,
        incoming_things& incoming,
        unsigned long max_content_length
    )
    {
        using namespace std;
        using namespace http_impl;
        read_with_limit(in, incoming.request_type, ' ');

        // get the path
        read_with_limit(in, incoming.path, ' ');

        // Get the HTTP/1.1 - Ignore for now...
        read_with_limit(in, incoming.protocol);

        key_value_map& incoming_headers = incoming.headers;
        key_value_map& cookies          = incoming.cookies;
        std::string& path               = incoming.path;
        std::string& content_type       = incoming.content_type;
        unsigned long content_length = 0;

        string line;
        read_with_limit(in, line);
        string first_part_of_header;
        string::size_type position_of_double_point;
        // now loop over all the incoming_headers
        while (line.size() > 2)
        {
            position_of_double_point = line.find_first_of(':');
            if ( position_of_double_point != string::npos )
            {
                first_part_of_header = dlib::trim(line.substr(0, position_of_double_point));

                if ( !incoming_headers[first_part_of_header].empty() )
                    incoming_headers[ first_part_of_header ] += " ";
                incoming_headers[first_part_of_header] += dlib::trim(line.substr(position_of_double_point+1));

                // look for Content-Type:
                if (line.size() > 14 && strings_equal_ignore_case(line, "Content-Type:", 13))
                {
                    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 && strings_equal_ignore_case(line, "Content-Length:", 15))
                {
                    istringstream sin(line.substr(16));
                    sin >> content_length;
                    if (!sin)
                    {
                        throw http_parse_error("Invalid Content-Length of '" + line.substr(16) + "'", 411);
                    }

                    if (content_length > max_content_length)
                    {
                        std::ostringstream sout;
                        sout << "Content-Length of post back is too large.  It must be less than " << max_content_length;
                        throw http_parse_error(sout.str(), 413);
                    }
                }
                // look for any cookies
                else if (line.size() > 6 && strings_equal_ignore_case(line, "Cookie:", 7))
                {
                    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] == ';')
                            {
                                cookies[urldecode(key)] = urldecode(value);
                                seen_equal_sign = false;
                                seen_key_start = false;
                                key.clear();
                                value.clear();
                            }
                            else
                            {
                                value += line[pos];
                            }
                        }
                    }
                    if (key.size() > 0)
                    {
                        cookies[urldecode(key)] = urldecode(value);
                        key.clear();
                        value.clear();
                    }
                }
            } // no ':' in it!
            read_with_limit(in, line);
        } // while (line.size() > 2 )


        // If there is data being posted back to us as a query string then
        // pick out the queries using parse_url.
        if ((strings_equal_ignore_case(incoming.request_type, "POST") || 
             strings_equal_ignore_case(incoming.request_type, "PUT")) && 
            strings_equal_ignore_case(left_substr(content_type,";"), "application/x-www-form-urlencoded"))
        {
            if (content_length > 0)
            {
                incoming.body.resize(content_length);
                in.read(&incoming.body[0],content_length);
            }
            parse_url(incoming.body, incoming.queries);
        }

        string::size_type pos = path.find_first_of("?");
        if (pos != string::npos)
        {
            parse_url(path.substr(pos+1), incoming.queries);
        }


        if (!in)
            throw http_parse_error("Error parsing HTTP request", 500);

        return content_length;
    }

// ----------------------------------------------------------------------------------------

    void read_body (
        std::istream& in,
        incoming_things& incoming
    )
    {
        // if the body hasn't already been loaded and there is data to load
        if (incoming.body.size() == 0 &&
            incoming.headers.count("Content-Length") != 0)
        {
            const unsigned long content_length = string_cast<unsigned long>(incoming.headers["Content-Length"]);

            incoming.body.resize(content_length);
            if (content_length > 0)
            {
                in.read(&incoming.body[0],content_length);
            }
        }
    }

// ----------------------------------------------------------------------------------------

    void write_http_response (
        std::ostream& out,
        outgoing_things outgoing,
        const std::string& result
    )
    {
        using namespace http_impl;
        key_value_map& new_cookies      = outgoing.cookies;
        key_value_map& response_headers = outgoing.headers;

        // only send this header if the user hasn't told us to send another kind
        bool has_content_type = false, has_location = false;
        for(key_value_map::const_iterator ci = response_headers.begin(); ci != response_headers.end(); ++ci )
        {
            if ( !has_content_type && strings_equal_ignore_case(ci->first , "content-type") )
            {
                has_content_type = true;
            }
            else if ( !has_location && strings_equal_ignore_case(ci->first , "location") )
            {
                has_location = true;
            }
        }

        if ( has_location )
        {
            outgoing.http_return = 302;
        }

        if ( !has_content_type )
        {
            response_headers["Content-Type"] = "text/html";
        }

        response_headers["Content-Length"] = cast_to_string(result.size());

        out << "HTTP/1.0 " << outgoing.http_return << " " << outgoing.http_return_status << "\r\n";

        // Set any new headers
        for(key_value_map::const_iterator ci = response_headers.begin(); ci != response_headers.end(); ++ci )
        {
            out << ci->first << ": " << ci->second << "\r\n";
        }

        // set any cookies 
        for(key_value_map::const_iterator ci = new_cookies.begin(); ci != new_cookies.end(); ++ci )
        {
            out << "Set-Cookie: " << urlencode(ci->first) << '=' << urlencode(ci->second) << "\r\n";
        }
        out << "\r\n" << result;
    }

// ----------------------------------------------------------------------------------------

    void write_http_response (
        std::ostream& out,
        const http_parse_error& e 
    )
    {
        outgoing_things outgoing;
        outgoing.http_return = e.http_error_code;
        outgoing.http_return_status = e.what();
        write_http_response(out, outgoing, std::string("Error processing request: ") + e.what());
    }

// ----------------------------------------------------------------------------------------

    void write_http_response (
        std::ostream& out,
        const std::exception& e 
    )
    {
        outgoing_things outgoing;
        outgoing.http_return = 500;
        outgoing.http_return_status = e.what();
        write_http_response(out, outgoing, std::string("Error processing request: ") + e.what());
    }

// ----------------------------------------------------------------------------------------

    const logger server_http::dlog("dlib.server_http");

// ----------------------------------------------------------------------------------------

}

#endif // DLIB_SERVER_HTTP_CPp_

