/*
 * 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-2016 ForgeRock AS.
 */

package org.forgerock.http.protocol;

import static org.forgerock.http.util.Json.readJson;
import static org.forgerock.http.util.Json.writeJson;
import static java.nio.charset.StandardCharsets.ISO_8859_1;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.forgerock.util.Utils.closeSilently;

import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.StringWriter;
import java.io.UnsupportedEncodingException;
import java.io.Writer;
import java.nio.charset.Charset;

import org.forgerock.http.header.ContentEncodingHeader;
import org.forgerock.http.header.ContentLengthHeader;
import org.forgerock.http.header.ContentTypeHeader;
import org.forgerock.http.io.BranchingInputStream;
import org.forgerock.http.io.IO;

/**
 * Message content. An entity wraps a BranchingInputStream and provides various
 * convenience methods for accessing the content, transparently handling content
 * encoding. The underlying input stream can be branched in order to perform
 * repeated reads of the data. This is achieved by either calling
 * {@link #push()}, {@link #newDecodedContentReader(Charset)}, or
 * {@link #newDecodedContentInputStream()}. The branch can then be closed by
 * calling {@link #pop} on the entity, or {@code close()} on the returned
 * {@link #newDecodedContentReader(Charset) BufferedReader} or
 * {@link #newDecodedContentInputStream() InputStream}. Calling {@link #close}
 * on the entity fully closes the input stream invaliding any branches in the
 * process.
 * <p>
 * Several convenience methods are provided for accessing the entity as either
 * {@link #getBytes() byte}, {@link #getString() string}, or {@link #getJson()
 * JSON} content.
 */
public final class Entity implements Closeable {

    /*
     * Implementation note: this class lazily creates the alternative string and
     * json representations. Updates to the json content, string content, bytes,
     * or input stream invalidates the other representations accordingly. The
     * setters cascade updates in the following order: setJson() -> setString()
     * -> setBytes() -> setRawInputStream().
     */

    /** The Content-Type used when setting the entity to JSON. */
    static final String APPLICATION_JSON_CHARSET_UTF_8 = "application/json; charset=UTF-8";

    /** Default content stream. */
    private static final BranchingInputStream EMPTY_STREAM = IO
            .newBranchingInputStream(new byte[0]);

    /** The encapsulating Message which may have content encoding headers. */
    private final Message message;

    /** The input stream from which all branches are created. */
    private BranchingInputStream trunk;

    /** The most recently created branch. */
    private BranchingInputStream head;

    /** Cached and lazily created JSON representation of the entity. */
    private Object json;

    /** Cached and lazily created String representation of the entity. */
    private String string;

    Entity(final Message message) {
        this.message = message;
        setEmpty();
    }

    /**
     * Defensive copy constructor.
     */
    Entity(final Message message, final Entity entity) throws IOException {
        this.message = message;
        setRawContentInputStream(entity.trunk.copy());
    }

    /**
     * Returns {@code true} if this entity's raw content is empty.
     *
     * @return {@code true} if this entity's raw content is empty.
     */
    public boolean isRawContentEmpty() {
        return isDecodedContentEmpty();
    }

    /**
     * Returns {@code true} if this entity's decoded content is empty.
     *
     * @return {@code true} if this entity's decoded content is empty.
     */
    public boolean isDecodedContentEmpty() {
        try {
            push();
            try {
                return head.read() == -1;
            } finally {
                pop();
            }
        } catch (IOException e) {
            return true;
        }
    }

    /**
     * Mark this entity as being empty.
     */
    public void setEmpty() {
        setRawContentInputStream(EMPTY_STREAM);
    }

    /**
     * Closes all resources associated with this entity. Any open streams will
     * be closed, and the underlying content reset back to a zero length.
     */
    @Override
    public void close() {
        setEmpty();
    }

    /**
     * Copies the decoded content of this entity to the provided writer. After
     * the method returns it will no longer be possible to read data from this
     * entity. This method does not push or pop branches. It does, however,
     * decode the content according to the {@code Content-Encoding} header if it
     * is present in the message.
     *
     * @param out
     *            The destination writer.
     * @throws IOException
     *             If an IO error occurred while copying the decoded content.
     */
    public void copyDecodedContentTo(final OutputStream out) throws IOException {
        IO.stream(getDecodedInputStream(head), out);
        out.flush();
    }

    /**
     * Copies the decoded content of this entity to the provided writer. After
     * the method returns it will no longer be possible to read data from this
     * entity. This method does not push or pop branches. It does, however,
     * decode the content according to the {@code Content-Encoding} and
     * {@code Content-Type} headers if they are present in the message.
     *
     * @param out
     *            The destination writer.
     * @throws IOException
     *             If an IO error occurred while copying the decoded content.
     */
    public void copyDecodedContentTo(final Writer out) throws IOException {
        IO.stream(getBufferedReader(head, null), out);
        out.flush();
    }

    /**
     * Copies the raw content of this entity to the provided output stream.
     * After the method returns it will no longer be possible to read data from
     * this entity. This method does not push or pop branches nor does it
     * perform any decoding of the raw data.
     *
     * @param out
     *            The destination output stream.
     * @throws IOException
     *             If an IO error occurred while copying the raw content.
     */
    public void copyRawContentTo(final OutputStream out) throws IOException {
        IO.stream(head, out);
        out.flush();
    }

    /**
     * Returns a byte array containing a copy of the decoded content of this
     * entity. Calling this method does not change the state of the underlying
     * input stream. Subsequent changes to the content of this entity will not
     * be reflected in the returned byte array, nor will changes in the returned
     * byte array be reflected in the content.
     *
     * @return A byte array containing a copy of the decoded content of this
     *         entity (never {@code null}).
     * @throws IOException
     *             If an IO error occurred while reading the content.
     */
    public byte[] getBytes() throws IOException {
        push();
        try {
            final ByteArrayOutputStream bytes = new ByteArrayOutputStream();
            copyDecodedContentTo(bytes);
            return bytes.toByteArray();
        } finally {
            pop();
        }
    }

    /**
     * Returns the content of this entity decoded as a JSON object. Calling this
     * method does not change the state of the underlying input stream.
     * Subsequent changes to the content of this entity will not be reflected in
     * the returned JSON object, nor will changes in the returned JSON object be
     * reflected in the content.
     *
     * @return The content of this entity decoded as a JSON object, which will
     *         be {@code null} only if the content represents the JSON
     *         {@code null} value.
     * @throws IOException
     *             If an IO error occurred while reading the content or if the
     *             JSON is malformed.
     */
    public Object getJson() throws IOException {
        if (json == null) {
            try (BufferedReader reader = newDecodedContentReader(UTF_8) /* RFC 7159 */) {
                json = readJson(reader);
            }
        }
        return json;
    }

    /**
     * Returns an input stream representing the raw content of this entity.
     * Reading from the input stream will update the state of this entity.
     *
     * @return An input stream representing the raw content of this entity.
     */
    public InputStream getRawContentInputStream() {
        return head;
    }

    /**
     * Returns the content of this entity decoded as a string. Calling this
     * method does not change the state of the underlying input stream.
     * Subsequent changes to the content of this entity will not be reflected in
     * the returned string, nor will changes in the returned string be reflected
     * in the content.
     *
     * @return The content of this entity decoded as a string (never
     *         {@code null}).
     * @throws IOException
     *             If an IO error occurred while reading the content.
     */
    public String getString() throws IOException {
        if (string == null) {
            push();
            try {
                final StringWriter writer = new StringWriter();
                copyDecodedContentTo(writer);
                string = writer.toString();
            } finally {
                pop();
            }
        }
        return string;
    }

    /**
     * Returns a branched input stream representing the decoded content of this
     * entity. Reading from the returned input stream will NOT update the state
     * of this entity.
     * <p>
     * The entity will be decompressed based on any codings that are specified
     * in the {@code Content-Encoding} header.
     * <p>
     * <b>Note:</b> The caller is responsible for calling the input stream's
     * {@code close} method when it is finished reading the entity.
     *
     * @return A buffered input stream for reading the decoded entity.
     * @throws UnsupportedEncodingException
     *             If content encoding are not supported.
     * @throws IOException
     *             If an IO error occurred while reading the content.
     */
    public InputStream newDecodedContentInputStream() throws IOException {
        final BranchingInputStream headBranch = head.branch();
        try {
            return getDecodedInputStream(headBranch);
        } catch (final IOException e) {
            closeSilently(headBranch);
            throw e;
        }
    }

    /**
     * Returns a branched reader representing the decoded content of this
     * entity. Reading from the returned reader will NOT update the state of
     * this entity.
     * <p>
     * The entity will be decoded and/or decompressed based on any codings that
     * are specified in the {@code Content-Encoding} header.
     * <p>
     * If {@code charset} is not {@code null} then it will be used to decode the
     * entity, otherwise the character set specified in the message's
     * {@code Content-Type} header (if present) will be used, otherwise the
     * default {@code ISO-8859-1} character set.
     * <p>
     * <b>Note:</b> The caller is responsible for calling the reader's
     * {@code close} method when it is finished reading the entity.
     *
     * @param charset
     *            The character set to decode with, or message-specified or
     *            default if {@code null}.
     * @return A buffered reader for reading the decoded entity.
     * @throws UnsupportedEncodingException
     *             If content encoding or charset are not supported.
     * @throws IOException
     *             If an IO error occurred while reading the content.
     */
    public BufferedReader newDecodedContentReader(final Charset charset)
            throws IOException {
        final BranchingInputStream headBranch = head.branch();
        try {
            return getBufferedReader(headBranch, charset);
        } catch (final IOException e) {
            closeSilently(headBranch);
            throw e;
        }
    }

    /**
     * Restores the underlying input stream to the state it had immediately
     * before the last call to {@link #push}.
     */
    public void pop() {
        closeSilently(head);
        head = head.parent();
    }

    /**
     * Saves the current position of the underlying input stream and creates a
     * new buffered input stream. Subsequent attempts to read from this entity,
     * e.g. using {@link #copyRawContentTo(OutputStream) copyRawContentTo} or
     * {@code #getRawInputStream()}, will not impact the saved state.
     * <p>
     * Use the {@link #pop} method to restore the entity to the previous state
     * it had before this method was called.
     *
     * @throws IOException
     *             If this entity has been closed.
     */
    public void push() throws IOException {
        head = head.branch();
    }

    /**
     * Sets the content of this entity to the raw data contained in the provided
     * byte array. Calling this method will close any existing streams
     * associated with the entity. Also sets the {@code Content-Length} header,
     * overwriting any existing header.
     * <p>
     * Note: This method does not attempt to encode the entity based-on any
     * codings specified in the {@code Content-Encoding} header.
     *
     * @param value
     *            A byte array containing the raw data.
     */
    public void setBytes(final byte[] value) {
        if (value == null || value.length == 0) {
            message.getHeaders().put(ContentLengthHeader.NAME, 0);
            setEmpty();
        } else {
            message.getHeaders().put(ContentLengthHeader.NAME, value.length);
            setRawContentInputStream(IO.newBranchingInputStream(value));
        }
    }

    /**
     * Sets the content of this entity to the JSON representation of the
     * provided object. Calling this method will close any existing streams
     * associated with the entity. Also sets the {@code Content-Type} and
     * {@code Content-Length} headers, overwriting any existing header.
     * <p>
     * Note: This method does not attempt to encode the entity based-on any
     * codings specified in the {@code Content-Encoding} header.
     *
     * @param value
     *            The object whose JSON representation is to be store in this
     *            entity.
     */
    public void setJson(final Object value) {
        message.getHeaders().put(ContentTypeHeader.NAME, APPLICATION_JSON_CHARSET_UTF_8);
        try {
            setBytes(writeJson(value));
        } catch (IOException e) {
            // TODO do something better than a runtime exception :)
            throw new RuntimeException("Cannot produce JSON from " + value, e);
        }
        json = value;
    }

    /**
     * Sets the content of this entity to the provided input stream. Calling
     * this method will close any existing streams associated with the entity.
     * No headers will be set.
     *
     * @param is
     *            The input stream.
     */
    public void setRawContentInputStream(final BranchingInputStream is) {
        closeSilently(trunk); // Closes all sub-branches
        trunk = is != null ? is : EMPTY_STREAM;
        head = trunk;
        string = null;
        json = null;
    }

    /**
     * Sets the content of this entity to the provided string. Calling this
     * method will close any existing streams associated with the entity. Also
     * sets the {@code Content-Length} header, overwriting any existing header.
     * <p>
     * The character set specified in the message's {@code Content-Type} header
     * (if present) will be used, otherwise the default {@code ISO-8859-1}
     * character set.
     * <p>
     * Note: This method does not attempt to encode the entity based-on any
     * codings specified in the {@code Content-Encoding} header.
     *
     * @param value
     *            The string whose value is to be store in this entity.
     */
    public void setString(final String value) {
        setBytes(value != null ? value.getBytes(cs(null)) : null);
        string = value;
    }

    /**
     * Returns the content of this entity decoded as a string. Calling this
     * method does not change the state of the underlying input stream.
     *
     * @return The content of this entity decoded as a string (never
     *         {@code null}).
     */
    @Override
    public String toString() {
        try {
            return getString();
        } catch (final IOException e) {
            return e.getMessage();
        }
    }

    private Charset cs(final Charset charset) {
        if (charset != null) {
            return charset;
        }
        // use Content-Type charset if not explicitly specified
        final Charset contentType = ContentTypeHeader.valueOf(message).getCharset();
        if (contentType != null) {
            return contentType;
        }
        // use default per RFC 2616 if not resolved
        return ISO_8859_1;
    }

    private BufferedReader getBufferedReader(final InputStream is, final Charset charset)
            throws IOException {
        return new BufferedReader(new InputStreamReader(getDecodedInputStream(is), cs(charset)));
    }

    private InputStream getDecodedInputStream(final InputStream is)
            throws IOException {
        return ContentEncodingHeader.valueOf(message).decode(is);
    }
}
