/*
 * Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved.
 * ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 */


import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.PushbackInputStream;
import java.util.Map;
import java.util.logging.Level;

/**
 * Representation of HTTP request.
 *
 */
class HttpRequest extends HttpData {

    HttpRequest.METHOD method;

    private String requestUri;

    protected byte[] bytes;

    protected HttpRequest() {

    }

    /**
     * Build HTTP request instance form raw data.
     *
     * @param is Input stream that provides raw data
     * @return HttpRequest instance
     * @throws Exception If something is wrong
     */
    public static HttpRequest build(InputStream is) throws Exception {
        HttpRequest httpRequest = new HttpRequest();
        httpRequest.init(is);
        return httpRequest;
    }

    private void init(InputStream is) throws Exception {
        parseRequestLine(readLine(is));

        // parse headers
        while (true) {
            String line = readLine(is);
            if (line == null || line.isEmpty()) {
                break;
            }
            parseHeader(line);
        }

        // determine lenght of data in request
        String headerValue = getHeader("Content-Length");
        int contentLength = 0;
        if (headerValue != null) {
            contentLength = Integer.valueOf(headerValue);
        }

        // read request data
        body = new byte[contentLength];
        int read = readBytes(is, body, contentLength);
        if (contentLength != read) {
            throw new Exception(
                    "Content-length (" + contentLength + ") is not equal actual length of read data (" + read + ")");
        }
    }

    /**
     * Read next ASCII string from input stream.
     *
     * The method stops reading the string if one of the following events is
     * happened: - zero symbol was read - CRLF sequence was read - LF symbol was
     * read
     *
     * @param is Input stream
     * @return Next read string
     * @throws IOException If something is wrong
     */
    private String readLine(InputStream is) throws IOException {
        StringBuilder sb = new StringBuilder();
        PushbackInputStream pbis = new PushbackInputStream(is);
        int current_char;
        loop: while ((current_char = pbis.read()) > 0) {
            switch (current_char) {
            case 0:
                // end of line
                break loop;
            case CR:
                // check CRLF separator
                if (pbis.available() > 0) {
                    int next_char = pbis.read();
                    if (next_char != LF)
                        pbis.unread(next_char);
                }
                break loop;
            case LF:
                break loop;
            default:
                sb.append((char) current_char);
            }
        }

        return sb.toString();
    }

    private void parseHeader(String headerLine) throws Exception {
        logger.log(Level.FINE, "Parse HTTP header line: {0}", headerLine);
        int index = headerLine.indexOf(":");
        if (index <= 0)
            throw new Exception("Cannot parse header: \"" + headerLine + "\"");

        String headerName = headerLine.substring(0, index).trim().toLowerCase();
        String headerValue = headerLine.substring(index + 1);

        headers.put(headerName, headerValue.trim());
    }

    private void parseRequestLine(String requestLine) throws Exception {
        logger.log(Level.FINE, "Parse HTTP request line: {0}", requestLine);
        String[] parts = requestLine.split("\\s");

        if (parts.length < 3)
            throw new Exception("Cannot parse request line: \"" + requestLine + "\"");

        method = HttpRequest.METHOD.valueOf(parts[0]);
        if (method == null)
            throw new Exception("Cannot parse request line: \"" + requestLine + "\". Invalid method.");

        requestUri = parts[1];
        httpVersion = parts[2];
    }

    public HttpRequest.METHOD getMethod() {
        return method;
    }

    public String getRequestUri() {
        return requestUri;
    }

    @Override
    public String toString() {
        return "HttpRequest " + "[" + "method = " + method + ", requestUri = " + requestUri + ", httpVersion = "
                + httpVersion + ", headers = " + headers.toString() + ", body (length = " + body.length + ") = {" + body
                + "}" + "]";
    }

    public byte[] getBytes() throws IOException {
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        bos.write(method.toString().getBytes());
        bos.write(" ".getBytes());
        bos.write(requestUri.getBytes());
        bos.write(" ".getBytes());
        bos.write(httpVersion.getBytes());
        bos.write(CRLF_BYTES);
        for (Map.Entry<String, String> headerEntry : headers.entrySet()) {
            bos.write(headerEntry.getKey().getBytes());
            bos.write(": ".getBytes());
            bos.write(headerEntry.getValue().getBytes());
            bos.write(CRLF_BYTES);
        }
        bos.write(CRLF_BYTES);
        bos.write(body);
        bos.flush();
        return bos.toByteArray();
    }

}
