/*
 * The contents of this file are subject to the terms of the Common Development and
 * Distribution License (the License). You may not use this file except in compliance with the
 * License.
 *
 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
 * specific language governing permission and limitations under the License.
 *
 * When distributing Covered Software, include this CDDL Header Notice in each file and include
 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
 * Header, with the fields enclosed by brackets [] replaced by your own identifying
 * information: "Portions copyright [year] [name of copyright owner]".
 *
 * Copyright 2014-2015 ForgeRock AS.
 */

package org.forgerock.http;

import static java.util.Collections.*;
import static org.forgerock.http.util.Paths.joinPath;
import static org.forgerock.http.util.Uris.*;

import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.List;

import org.forgerock.http.util.Paths;

/**
 * A MutableUri is a modifiable {@link URI} substitute.
 * Unlike URIs, which are immutable, a MutableUri can have its fields updated independently.
 * That makes it easier if you just want to change a element of an Uri.
 *
 * @see URI
 */
public final class MutableUri implements Comparable<MutableUri> {


    /**
     * Factory method for avoiding typing {@code new MutableUri("http://...")}.
     * @param uri URL encoded URI
     * @return a new MutableUri instance
     * @throws URISyntaxException if the given Uri is not well-formed
     */
    public static MutableUri uri(String uri) throws URISyntaxException {
        return new MutableUri(uri);
    }

    /**
     * The real URI, hidden by this class. Recreated each time a field is updated.
     */
    private URI uri;
    private List<String> pathElements;

    /**
     * Builds a new MutableUri using the given URI.
     * @param uri URI
     */
    public MutableUri(final URI uri) {
        this.uri = uri;
        setPathElements(uri.getRawPath());
    }

    /**
     * Builds a new MutableUri with deep copy.
     * @param mutableUri URI
     */
    public MutableUri(final MutableUri mutableUri) {
        this(mutableUri.asURI());
    }

    /**
     * Builds a new MutableUri using the given URL encoded String URI.
     * @param uri URL encoded URI
     * @throws URISyntaxException if the given Uri is not well-formed
     */
    public MutableUri(final String uri) throws URISyntaxException {
        this(new URI(uri));
    }

    /**
     * Builds a new MutableUri using the given fields values (decoded values).
     * @param scheme Scheme name
     * @param userInfo User name and authorization information
     * @param host Host name
     * @param port Port number
     * @param path Path
     * @param query Query
     * @param fragment Fragment
     * @throws URISyntaxException if the produced URI is not well-formed
     */
    public MutableUri(String scheme,
                      String userInfo,
                      String host,
                      int port,
                      String path,
                      String query,
                      String fragment)
            throws URISyntaxException {
        this(new URI(scheme, userInfo, host, port, path, query, fragment));
    }

    /**
     * Returns the equivalent {@link URI} instance.
     * @return the equivalent {@link URI} instance.
     */
    public URI asURI() {
        return uri;
    }

    /**
     * Returns the scheme name.
     * @return the scheme name.
     */
    public String getScheme() {
        return uri.getScheme();
    }

    /**
     * Update the scheme of this MutableUri.
     * @param scheme new scheme name
     * @throws URISyntaxException if the new equivalent URI is invalid
     */
    public void setScheme(final String scheme) throws URISyntaxException {
        this.uri = create(scheme,
                          uri.getRawUserInfo(),
                          uri.getHost(),
                          uri.getPort(),
                          uri.getRawPath(),
                          uri.getRawQuery(),
                          uri.getRawFragment());
    }

    /**
     * Returns the user info element.
     * @return the user info element.
     */
    public String getUserInfo() {
        return uri.getUserInfo();
    }

    /**
     * Returns the raw (encoded) user info element.
     * @return the raw user info element.
     */
    public String getRawUserInfo() {
        return uri.getRawUserInfo();
    }

    /**
     * Update the user info (not encoded) of this MutableUri.
     * @param userInfo new user info element (not encoded)
     * @throws URISyntaxException if the new equivalent URI is invalid
     */
    public void setUserInfo(final String userInfo) throws URISyntaxException {
        URI other = new URI(null, userInfo, "ignored", -1, null, null, null);
        setRawUserInfo(other.getRawUserInfo());
    }

    /**
     * Update the user info (encoded) of this MutableUri.
     * @param rawUserInfo new user info element (encoded)
     * @throws URISyntaxException if the new equivalent URI is invalid
     */
    public void setRawUserInfo(String rawUserInfo) throws URISyntaxException {
        uri = create(uri.getScheme(),
                     rawUserInfo,
                     uri.getHost(),
                     uri.getPort(),
                     uri.getRawPath(),
                     uri.getRawQuery(),
                     uri.getRawFragment());
    }

    /**
     * Returns the host element.
     * @return the host element.
     */
    public String getHost() {
        return uri.getHost();
    }

    /**
     * Update the host name of this MutableUri.
     * @param host new host element
     * @throws URISyntaxException if the new equivalent URI is invalid
     */
    public void setHost(final String host) throws URISyntaxException {
        this.uri = create(uri.getScheme(),
                          uri.getRawUserInfo(),
                          host,
                          uri.getPort(),
                          uri.getRawPath(),
                          uri.getRawQuery(),
                          uri.getRawFragment());
    }

    /**
     * Returns the port element.
     * @return the port element.
     */
    public int getPort() {
        return uri.getPort();
    }

    /**
     * Update the port of this MutableUri.
     * @param port new port number
     * @throws URISyntaxException if the new equivalent URI is invalid
     */
    public void setPort(final int port) throws URISyntaxException {
        uri = create(uri.getScheme(),
                     uri.getRawUserInfo(),
                     uri.getHost(),
                     port,
                     uri.getRawPath(),
                     uri.getRawQuery(),
                     uri.getRawFragment());
    }

    /**
     * Returns the path element.
     * @return the path element.
     */
    public String getPath() {
        return uri.getPath();
    }

    /**
     * Returns the raw (encoded) path element.
     * @return the raw path element.
     */
    public String getRawPath() {
        return uri.getRawPath();
    }

    /**
     * Update the path (not encoded) of this MutableUri.
     * @param path new path element (not encoded)
     * @throws URISyntaxException if the new equivalent URI is invalid
     */
    public void setPath(final String path) throws URISyntaxException {
        URI other = new URI(null, null, "ignored", -1, path, null, null);
        setRawPath(other.getRawPath());
    }

    /**
     * Update the pah (encoded) of this MutableUri.
     * @param rawPath new path element (encoded)
     * @throws URISyntaxException if the new equivalent URI is invalid
     */
    public void setRawPath(String rawPath) throws URISyntaxException {
        uri = create(uri.getScheme(),
                     uri.getRawUserInfo(),
                     uri.getHost(),
                     uri.getPort(),
                     rawPath,
                     uri.getRawQuery(),
                     uri.getRawFragment());
        setPathElements(uri.getRawPath());
    }

    /**
     * Returns the path element.
     * @return the path element.
     */
    public String getQuery() {
        return uri.getQuery();
    }

    /**
     * Returns the raw (encoded) query element.
     * @return the raw query element.
     */
    public String getRawQuery() {
        return uri.getRawQuery();
    }

    /**
     * Update the query string (not encoded) of this MutableUri.
     * @param query new query string element (not encoded)
     * @throws URISyntaxException if the new equivalent URI is invalid
     */
    public void setQuery(final String query) throws URISyntaxException {
        URI other = new URI(null, null, "ignored", -1, null, query, null);
        setRawQuery(other.getRawQuery());
    }

    /**
     * Update the query (encoded) of this MutableUri.
     * @param rawQuery new query element (encoded)
     * @throws URISyntaxException if the new equivalent URI is invalid
     */
    public void setRawQuery(String rawQuery) throws URISyntaxException {
        uri = create(uri.getScheme(),
                     uri.getRawUserInfo(),
                     uri.getHost(),
                     uri.getPort(),
                     uri.getRawPath(),
                     rawQuery,
                     uri.getRawFragment());
    }

    /**
     * Returns the fragment element.
     * @return the fragment element.
     */
    public String getFragment() {
        return uri.getFragment();
    }

    /**
     * Returns the raw (encoded) fragment element.
     * @return the raw fragment element.
     */
    public String getRawFragment() {
        return uri.getRawFragment();
    }

    /**
     * Update the fragment (not encoded) of this MutableUri.
     * @param fragment new fragment element (not encoded)
     * @throws URISyntaxException if the new equivalent URI is invalid
     */
    public void setFragment(final String fragment) throws URISyntaxException {
        URI other = new URI(null, null, "ignored", -1, null, null, fragment);
        setRawFragment(other.getRawFragment());
    }

    /**
     * Update the fragment (encoded) of this MutableUri.
     * @param rawFragment new framgent element (encoded)
     * @throws URISyntaxException if the new equivalent URI is invalid
     */
    public void setRawFragment(String rawFragment) throws URISyntaxException {
        uri = create(uri.getScheme(),
                     uri.getRawUserInfo(),
                     uri.getHost(),
                     uri.getPort(),
                     uri.getRawPath(),
                     uri.getRawQuery(),
                     rawFragment);
    }

    /**
     * Returns the authority compound element.
     * @return the authority compound element.
     */
    public String getAuthority() {
        return uri.getAuthority();
    }

    /**
     * Returns the raw (encoded) authority compound element.
     * @return the authority compound element.
     */
    public String getRawAuthority() {
        return uri.getRawAuthority();
    }

    /**
     * Sets the {@literal pathElements} from the URI path (encoded value).
     * <p>
     * This method does not set or recreate the {@literal uri}, this is the
     * responsibility of the method caller.
     *
     * @param rawPath The raw (URI-encoded) path.
     */
    private void setPathElements(final String rawPath) {
        this.pathElements = new ArrayList<String>(Paths.getPathElements(rawPath)) {
            @Override
            public String toString() {
                return joinPath(this);
            }
        };
    }

    /**
     * Return the URI path elements as an immutable {@code List}. The {@code toString} method of the
     * returned object will return the URL-encoded path elements joined with {@code "/"}.
     *
     * @return The URI path elements as an immutable {@code List}.
     */
    public List<String> getPathElements() {
        return unmodifiableList(pathElements);
    }

    /**
     * Changes the base scheme, host and port of this MutableUri to that specified in a base URI,
     * or leaves them unchanged if the base URI is {@code null}. This implementation only
     * uses scheme, host and port. The remaining components of the URI remain intact.
     *
     * @param base the URI to base the other URI on.
     * @return this (rebased) instance
     */
    public MutableUri rebase(MutableUri base) {
        if (base == null) {
            return this;
        }
        String scheme = base.getScheme();
        String host = base.getHost();
        int port = base.getPort();
        if (scheme == null || host == null) {
            return this;
        }
        try {
            setScheme(scheme);
            setHost(host);
            setPort(port);
        } catch (URISyntaxException e) {
            throw new IllegalStateException(e);
        }
        return this;
    }

    /**
     * Changes the base scheme, host and port of this MutableUri to that specified in a base URI,
     * or leaves them unchanged if the base URI is {@code null}. This implementation only
     * uses scheme, host and port. The remaining components of the URI remain intact.
     *
     * @param base the URI to base the other URI on.
     * @return this (rebased) instance
     */
    public MutableUri rebase(URI base) {
        return rebase(new MutableUri(base));
    }

    @Override
    public int compareTo(final MutableUri o) {
        return asURI().compareTo(o.asURI());
    }

    /**
     * Relativizes the given URI against this URI.
     * @param uri the uri to relativizes against this instance
     * @return this instance (mutated)
     * @see URI#relativize(URI)
     */
    public MutableUri relativize(final MutableUri uri) {
        this.uri = this.uri.relativize(uri.asURI());
        setPathElements(this.uri.getRawPath());
        return this;
    }

    /**
     * Resolves the given URI against this URI.
     * @param uri the uri to resolve against this instance
     * @return this instance (mutated)
     * @see URI#resolve(URI)
     */
    public MutableUri resolve(final MutableUri uri) {
        this.uri = this.uri.resolve(uri.asURI());
        setPathElements(this.uri.getRawPath());
        return this;
    }

    @Override
    public String toString() {
        return uri.toString();
    }

    /**
     * Returns the content of this URI as a US-ASCII string.
     * @return the content of this URI as a US-ASCII string.
     */
    public String toASCIIString() {
        return uri.toASCIIString();
    }

    @Override
    public boolean equals(final Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || !(o instanceof MutableUri)) {
            return false;
        }
        MutableUri that = (MutableUri) o;
        return uri.equals(that.uri);

    }

    @Override
    public int hashCode() {
        return uri.hashCode();
    }
}
