package rocks.bastion.core;

import org.apache.http.entity.ContentType;

import java.util.Collection;
import java.util.LinkedList;
import java.util.Objects;
import java.util.Optional;

/**
 * <p>
 * Contains the common attributes which usually appear in all HTTP requests. This class is intended to be used as an object
 * inside implementations of {@link HttpRequest}. Using inheritance by composition, we avoid repeating the handling for
 * HTTP headers, query parameters and route parameters.
 * </p>
 * <p>
 * While not being an {@link HttpRequest}, this class provides methods which are similar to those found in that interface
 * so that classes which use the {@linkplain CommonRequestAttributes} can delegate the {@link HttpRequest} implementing
 * methods to this object.
 * </p>
 *
 * @author <a href="mailto:[email protected]">Kyle</a>
 */
public class CommonRequestAttributes {

    /**
     * Represents an empty HTTP content body.
     */
    public static final String EMPTY_BODY = "";

    private String name;
    private String url;
    private HttpMethod method;
    private ContentType contentType;
    private Collection<ApiHeader> headers;
    private Collection<ApiQueryParam> queryParams;
    private Collection<RouteParam> routeParams;
    private Object body;
    private long timeout;

    /**
     * Constructs a new instance of this object containing the following initial defaults:
     * <ul>
     * <li>HTTP method: Supplied as an argument this this constructor.</li>
     * <li>URL: Supplied as an argument this this constructor.</li>
     * <li>Name: Initialised to be the HTTP method concatenated with the URL.</li>
     * <li>Content-type: Initialised to the value of {@link ContentType#TEXT_PLAIN}.</li>
     * <li>Headers: Initialised to the empty collection of headers.</li>
     * <li>Query parameters: Initialised to the empty collection of query parameters.</li>
     * <li>Route parameters: Initialised to the empty collection of route parameters.</li>
     * <li>Timeout: Falls back to globally configured timeout</li>
     * </ul>
     *
     * @param method The HTTP method to use for a request. Cannot be {@literal null}.
     * @param url    The URL to use for a request. Cannot be {@literal null}.
     * @param body   The body to use for a request. Cannot be {@literal null}.
     */
    public CommonRequestAttributes(HttpMethod method, String url, Object body) {
        Objects.requireNonNull(method);
        Objects.requireNonNull(url);

        this.method = method;
        this.url = url;
        name = method.getValue() + ' ' + url;
        contentType = ContentType.TEXT_PLAIN;
        headers = new LinkedList<>();
        queryParams = new LinkedList<>();
        routeParams = new LinkedList<>();
        timeout = HttpRequest.USE_GLOBAL_TIMEOUT;
        setBody(body);
    }

    /**
     * Change the HTTP method used by a request.
     *
     * @param method A non-{@literal null} HTTP method.
     */
    public void setMethod(HttpMethod method) {
        Objects.requireNonNull(method);
        this.method = method;
    }

    /**
     * Change the descriptive name used by a request.
     *
     * @param name A non-{@literal null} descriptive name.
     */
    public void setName(String name) {
        Objects.requireNonNull(name);
        this.name = name;
    }

    /**
     * Sets the URL to use for a request.
     *
     * @param url A non-{@literal null} URL to send with a request
     */
    public void setUrl(String url) {
        Objects.requireNonNull(url);
        this.url = url;
    }

    /**
     * Set the content-type that will be used for a request.
     *
     * @param contentType A content-type to use for a request. Use {@literal null} to send no content-type header.
     */
    public void setContentType(ContentType contentType) {
        this.contentType = contentType;
    }

    /**
     * Add a new HTTP header that will be sent with a request.
     *
     * @param name  A non-{@literal null} name for the new header
     * @param value A non-{@literal null} value for the new header
     */
    public void addHeader(String name, String value) {
        Objects.requireNonNull(name);
        Objects.requireNonNull(value);
        headers.add(new ApiHeader(name, value));
    }

    /**
     * Add a new HTTP query parameter that will be sent with the request.
     *
     * @param name  A non-{@literal null} name for the new query parameter
     * @param value A non-{@literal null} value for the new query parameter
     */
    public void addQueryParam(String name, String value) {
        Objects.requireNonNull(name);
        Objects.requireNonNull(value);
        queryParams.add(new ApiQueryParam(name, value));
    }

    /**
     * Add a new HTTP route parameter that will be sent with the request.
     *
     * @param name  A non-{@literal null} name for the new query parameter
     * @param value A non-{@literal null} value for the new query parameter
     */
    public void addRouteParam(String name, String value) {
        Objects.requireNonNull(name);
        Objects.requireNonNull(value);
        routeParams.add(new RouteParam(name, value));
    }

    /**
     * Sets the body content that will be sent by a request. You cannot set the body to {@literal null}; instead,
     * if you don't want to send any body content, pass in the {@link #EMPTY_BODY} constant.
     *
     * @param body A non-{@literal null} string to send as the body content
     */
    public void setBody(Object body) {
        Objects.requireNonNull(body);
        this.body = body;
    }

    /**
     * See {@link HttpRequest#name()} for information about this method.
     *
     * @return The descriptive name to use for a request
     */
    public String name() {
        return name;
    }

    /**
     * See {@link HttpRequest#url()} for information about this method.
     *
     * @return The URL to use for a request
     */
    public String url() {
        return url;
    }

    /**
     * See {@link HttpRequest#method()} for information about this method.
     *
     * @return The HTTP method to use for a request
     */
    public HttpMethod method() {
        return method;
    }

    /**
     * See {@link HttpRequest#contentType()} for information about this method.
     *
     * @return The content-type header, inside an {@link Optional#ofNullable(Object) Optional container} to use for a request.
     * An {@link Optional#empty() empty optional} is returned if no content type was set.
     */
    public Optional<ContentType> contentType() {
        return Optional.ofNullable(contentType);
    }

    /**
     * See {@link HttpRequest#headers()} for information about this method.
     *
     * @return The collection of headers to use for a request
     */
    public Collection<ApiHeader> headers() {
        return headers;
    }

    /**
     * See {@link HttpRequest#queryParams()} for information about this method.
     *
     * @return The collection of query parameters to use for a request
     */
    public Collection<ApiQueryParam> queryParams() {
        return queryParams;
    }

    /**
     * See {@link HttpRequest#routeParams()} for information about this method.
     *
     * @return The collection of route parameters to use for a request
     */
    public Collection<RouteParam> routeParams() {
        return routeParams;
    }

    /**
     * See {@link HttpRequest#body()} for information about this method.
     *
     * @return The body content to send with a request
     */
    public Object body() {
        return body;
    }

    /**
     * See {@link HttpRequest#timeout()}.
     *
     * @return the timeout for all phases of a request
     */
    public long timeout() {
        return timeout;
    }


    /**
     * Set the timeout for all phases of a request.
     * See {@link HttpRequest#timeout()} for more details.
     *
     * @param timeout the timeout (in milliseconds).
     */
    public void setTimeout(long timeout) {
        this.timeout = timeout;
    }
}