/*
 * @(#)HttpSession.java
 *
 * Copyright (c) 2004 JSCAPE
 * 1147 S. 53rd Pl., Mesa, Arizona, 85206, U.S.A.
 * All rights reserved.
 *
 * This software is the confidential and proprietary information of
 * JSCAPE. ("Confidential Information").  You shall not disclose such
 * Confidential Information and shall use it only in accordance with
 * the terms of the license agreement you entered into with JSCAPE.
 */
package com.jscape.inet.http;

import com.jscape.inet.https.Https;
import com.jscape.inet.mime.MimeException;
import com.jscape.inet.mime.MimeHeader;
import com.jscape.inet.util.URLParser;

import java.io.PrintStream;
import java.net.MalformedURLException;
import java.nio.charset.StandardCharsets;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Locale;
import java.util.Vector;

import static com.jscape.util.Parameters.assertNotNull;

/**
 * Implements the basic functionality of an HTTP client automatically managing all session data in memory.
 * <p/>
 * Example:<P>
 * <pre>
 * HttpSession session = new HttpSession();
 * HttpResponse response1 = session.getResponse("http://www.mydomain.com/page1");
 * HttpResponse response2 = session.getResponse("http://www.mydomain.com/page2");
 * HttpRequest page3 = new HttpRequest("http://www.mydomain.com/page3");
 * page3.addParameter("username","jsmith");
 * page3.addParameter("password","secret"); *
 * HttpResponse response3 = session.getResponse(page3);
 * </pre>
 */
public class HttpSession {

    private Hashtable cookies = new Hashtable();
    private Http http;
    private boolean allowCookies = true;
    private boolean debug = false;
    private PrintStream debugStream = System.out;

    /**
     * Creates a new HttpSession instance.
     *
     * @param http an Http instance.
     * @see com.jscape.inet.http.Http
     */
    public HttpSession(Http http) {
        this.http = http;
        this.http.setDebug(this.debug);
        this.http.setDebugStream(this.debugStream);
    }

    /**
     * Creates a new HttpSession instance.
     */
    public HttpSession() {
        this(new Http());
    }

    /**
     * Gets response from HTTP server.
     *
     * @param url    the web address to retrieve
     * @param method the method, valid values are GET and POST
     * @return an HttpResponse
     * @throws HttpException if an I/O or HTTP related error occurs
     */
    public HttpResponse getResponse(String url, String method)
            throws HttpException {
        synchronized (this) {
            return getResponse(new HttpRequest(url, method));
        }
    }

    /**
     * Gets response from HTTP server.
     *
     * @param url the web address to retrieve
     * @return an HttpResponse
     * @throws HttpException if an I/O or HTTP related error occurs
     */
    public HttpResponse getResponse(String url)
            throws HttpException {
        synchronized (this) {
            debug("getting response for - " + url);
            return getResponse(new HttpRequest(url));
        }
    }

    /**
     * Gets response from HTTP server.
     *
     * @param request an HttpRequest
     * @return an HttpResponse
     * @throws HttpException if an I/O or HTTP related error occurs
     */
    public HttpResponse getResponse(HttpRequest request)
            throws HttpException {
        synchronized (this) {
            // get url
            String url = request.getURL();

            if (url.toLowerCase(Locale.ENGLISH).startsWith("https")) {
                if (!(http instanceof Https)) {
                    http = new Https();
                }
            } else if (this.http instanceof Https) {
                this.http = new Http();
            }

            this.http.setDebug(this.debug);
            this.http.setDebugStream(this.debugStream);

            // add cookies to request
            addCookiesForRequest(request);

            // get response
            HttpResponse response = http.getResponse(request);

            // parse cookies from response
            if (allowCookies) {
                try {
                    handleCookies(request, response);
                } catch (MimeException me) {
                    throw new HttpException(me.getMessage(), me);
                }
            }

            // handle redirects
            while (response.getResponseCode() == 302) {
                // get Location header
                MimeHeader location = response.getHeader("Location");
                String redir = location.getValue();
                debug("got a redirect - " + redir);

                if (!redir.toLowerCase(Locale.ENGLISH).startsWith("http://") && !redir.toLowerCase(Locale.ENGLISH).startsWith("https://")) {
                    try {
                        URLParser parser = new URLParser(url);
                        StringBuffer tmpURL = new StringBuffer();
                        tmpURL.append(parser.getProtocol());
                        tmpURL.append("://");
                        tmpURL.append(parser.getHost());
                        if (parser.getPort() != 80
                            && parser.getPort() != 443) {
                            tmpURL.append(":" + parser.getPort());
                        }

                        if (redir.startsWith("/")) {
                            tmpURL.append(redir);
                        } else {
                            tmpURL.append(parser.getPath() + "/" + redir);
                        }
                        redir = tmpURL.toString();
                        debug("rewriting redirect header - " + redir);
                    } catch (MalformedURLException mfu) {
                        throw new HttpException(mfu.getMessage(), mfu);
                    }
                }

                // create new request based on Location header
                debug("redirecting to - " + redir);
                HttpRequest redirect = new HttpRequest(redir);

                // add cookies to request
                addCookiesForRequest(redirect);

                try {
                    debug("response before redirect - " + response.getBody());
                } catch (Exception e) {
                    e.printStackTrace();
                }

                // get new response
                try {
                    debug("--begin http request--");
                    debug(new String(redirect.getMessage(), StandardCharsets.UTF_8));
                    debug("--end http request--");
                } catch (Exception e) {
                    e.printStackTrace();
                }

                response = http.getResponse(redirect);
                try {
                    debug("response after redirect - " + response.getBody());
                } catch (Exception e) {
                    e.printStackTrace();
                }

                // parse cookies from response
                if (allowCookies) {
                    try {
                        debug(
                                "handling cookies for redirect - "
                                + redirect.getURL());
                        handleCookies(redirect, response);
                    } catch (MimeException me) {
                        throw new HttpException(me.getMessage(), me);
                    }
                }
            }
            return response;
        }
    }

    /**
     * Adds valid cookies in memory to HttpRequest
     *
     * @param request the request to add cookies to
     */
    private void addCookiesForRequest(HttpRequest request) {
        try {
            URLParser parser = new URLParser(request.getURL());
            String host = parser.getHost().toLowerCase(Locale.ENGLISH);

            // get cookies for host
            Vector cookiesForHost = getCookiesForHost(host);

            for (int i = 0; i < cookiesForHost.size(); ++i) {
                HttpCookie cookie = (HttpCookie) cookiesForHost.elementAt(i);
                request.addCookie(
                        cookie.getCookieName(),
                        cookie.getCookieValue());
                debug(
                        "adding cookie - "
                        + cookie.getCookieName()
                        + ":"
                        + cookie.getCookieValue());
            }
        } catch (Exception e) {
            e.printStackTrace();

        }
    }

    /**
     * Handles receipt of cookies from HTTP server
     *
     * @param request  the HttpRequest
     * @param response the HttpResposne
     * @throws MimeException if a MIME related error occurs
     */
    @SuppressWarnings("unchecked")
    private void handleCookies(HttpRequest request, HttpResponse response)
            throws MimeException {
        Enumeration e = response.getCookies();

        // loop thru all cookies returned in response
        while (e.hasMoreElements()) {
            HttpCookie cookie = (HttpCookie) e.nextElement();
            debug(
                    "got cookie - "
                    + cookie.getCookieName()
                    + ":"
                    + cookie.getCookieValue());
            String domain = cookie.getDomain();

            // set domain if null
            if (domain == null) {
                try {
                    URLParser parser = new URLParser(request.getURL());
                    domain = parser.getHost();
                    debug(
                            "domain not found for cookie, setting host for cookie: "
                            + domain);
                } catch (Exception ep) {
                    ep.printStackTrace();
                }
            }

            Vector cookiesForDomain = getCookiesForHost(domain);

            // replace cookies in vector
            boolean replaced = false;
            for (int i = 0; i < cookiesForDomain.size(); ++i) {
                HttpCookie tmp = (HttpCookie) cookiesForDomain.get(i);
                if (tmp.getCookieName().equals(cookie.getCookieName())) {
                    cookiesForDomain.set(i, cookie);
                    replaced = true;
                }
            }

            // if not replaced then add cookie to vector
            if (!replaced) {
                cookiesForDomain.add(cookie);
            }

            // replace cookies in hashtable
            debug("adding cookies for domain: "
                  + domain
                  + ":"
                  + cookiesForDomain.size());
            cookies.put(domain, cookiesForDomain);
        }
    }

    /**
     * Gets all cookies for the specified hostname
     *
     * @param host the hostname to find cookies for
     * @return a Vector of cookies
     */
    @SuppressWarnings("unchecked")
    public Vector getCookiesForHost(String host) {
        Vector cookiesForDomain = new Vector();
        synchronized (this) {
            // get cookies for domain
            Enumeration e = cookies.keys();

            while (e.hasMoreElements()) {
                String domain = ((String) e.nextElement()).toLowerCase(Locale.ENGLISH);
                if (host.endsWith(domain)) {
                    Vector values = (Vector) cookies.get(domain);
                    for (int i = 0; i < values.size(); ++i) {
                        cookiesForDomain.addElement(values.get(i));
                    }
                }
            }
        }
        return cookiesForDomain;
    }

    /**
     * Checks if cookies are allowed to be stored in memory.
     * Default is true.
     *
     * @return true if cookies are allowed, false otherwise
     */
    public boolean isAllowCookies() {
        return allowCookies;
    }

    /**
     * Sets whether cookies are allowed to be stored in memory.
     *
     * @param allowCookies true if cookies are allowed, false otherwise
     */
    public void setAllowCookies(boolean allowCookies) {
        this.allowCookies = allowCookies;

    }

    /**
     * Prints debug message to debug stream.
     *
     * @param msg the debug message
     */
    private void debug(String msg) {
        if (debug) {
            debugStream.println(msg);
        }
    }

    /**
     * Enables or disables debugging.
     * Default is true.
     *
     * @param debug true if enabled, false otherwise.
     */
    public void setDebug(boolean debug) {
        this.debug = debug;
        http.setDebug(debug);
    }

    /**
     * Checks if debugging is enabled.
     * Default is true.
     *
     * @return true if enabled, false otherwise
     */
    public boolean isDebug() {
        return debug;
    }

    /**
     * Sets debug stream to be used for debug messages.
     * Default is System.out
     *
     * @param debugStream a PrintStream
     * @see java.io.PrintStream
     */
    public void setDebugStream(PrintStream debugStream) {
        assertNotNull(debugStream);
        this.debugStream = debugStream;
        this.http.setDebugStream(this.debugStream);
    }

    /**
     * Gets debug stream used for debug messages.
     * Default is System.out
     *
     * @return a PrintStream
     * @see java.io.PrintStream
     */
    public PrintStream getDebugStream() {
        return this.debugStream;
    }

}
