/*
 *  Licensed to the Apache Software Foundation (ASF) under one or more
 *  contributor license agreements.  See the NOTICE file distributed with
 *  this work for additional information regarding copyright ownership.
 *  The ASF licenses this file to You under the Apache License, Version 2.0
 *  (the "License"); you may not use this file except in compliance with
 *  the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */
package org.apache.coyote;

import java.io.IOException;
import java.io.StringReader;
import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Supplier;

import jakarta.servlet.WriteListener;

import org.apache.juli.logging.Log;
import org.apache.juli.logging.LogFactory;
import org.apache.tomcat.util.buf.CharsetHolder;
import org.apache.tomcat.util.buf.MessageBytes;
import org.apache.tomcat.util.http.MimeHeaders;
import org.apache.tomcat.util.http.parser.MediaType;
import org.apache.tomcat.util.res.StringManager;

/**
 * Response object.
 *
 * @author James Duncan Davidson [duncan@eng.sun.com]
 * @author Jason Hunter [jch@eng.sun.com]
 * @author James Todd [gonzo@eng.sun.com]
 * @author Harish Prabandham
 * @author Hans Bergsten [hans@gefionsoftware.com]
 * @author Remy Maucherat
 */
public final class Response {

    private static final StringManager sm = StringManager.getManager(Response.class);
    private static final Log log = LogFactory.getLog(Response.class);


    // ----------------------------------------------------- Class Variables

    /**
     * Default locale as mandated by the spec.
     */
    private static final Locale DEFAULT_LOCALE = Locale.getDefault();


    // ----------------------------------------------------- Instance Variables

    /**
     * Status code.
     */
    int status = 200;


    /**
     * Status message.
     */
    String message = null;


    /**
     * Response headers.
     */
    final MimeHeaders headers = new MimeHeaders();


    private Supplier<Map<String,String>> trailerFieldsSupplier = null;

    /**
     * Associated output buffer.
     */
    OutputBuffer outputBuffer;


    /**
     * Notes.
     */
    final Object[] notes = new Object[Constants.MAX_NOTES];


    /**
     * Committed flag.
     */
    volatile boolean committed = false;


    /**
     * Action hook.
     */
    volatile ActionHook hook;


    /**
     * HTTP specific fields.
     */
    String contentType = null;
    String contentLanguage = null;
    private CharsetHolder charsetHolder = CharsetHolder.EMPTY;
    long contentLength = -1;
    private Locale locale = DEFAULT_LOCALE;

    // General information
    private long contentWritten = 0;
    private long commitTimeNanos = -1;

    /**
     * Holds response writing error exception.
     */
    private Exception errorException = null;

    /**
     * With the introduction of async processing and the possibility of non-container threads calling sendError()
     * tracking the current error state and ensuring that the correct error page is called becomes more complicated.
     * This state attribute helps by tracking the current error state and informing callers that attempt to change state
     * if the change was successful or if another thread got there first.
     *
     * <pre>
     * The state machine is very simple:
     *
     * 0 - NONE
     * 1 - NOT_REPORTED
     * 2 - REPORTED
     *
     *
     *   -->---->-- >NONE
     *   |   |        |
     *   |   |        | setError()
     *   ^   ^        |
     *   |   |       \|/
     *   |   |-<-NOT_REPORTED
     *   |            |
     *   ^            | report()
     *   |            |
     *   |           \|/
     *   |----<----REPORTED
     * </pre>
     */
    private final AtomicInteger errorState = new AtomicInteger(0);

    Request req;


    // ------------------------------------------------------------- Properties

    public Request getRequest() {
        return req;
    }

    public void setRequest(Request req) {
        this.req = req;
    }


    public void setOutputBuffer(OutputBuffer outputBuffer) {
        this.outputBuffer = outputBuffer;
    }


    public MimeHeaders getMimeHeaders() {
        return headers;
    }


    void setHook(ActionHook hook) {
        this.hook = hook;
    }


    // -------------------- Per-Response "notes" --------------------

    public void setNote(int pos, Object value) {
        notes[pos] = value;
    }


    public Object getNote(int pos) {
        return notes[pos];
    }


    // -------------------- Actions --------------------

    public void action(ActionCode actionCode, Object param) {
        if (hook != null) {
            hook.action(actionCode, Objects.requireNonNullElse(param, this));
        }
    }


    // -------------------- State --------------------

    public int getStatus() {
        return status;
    }


    /**
     * Set the response status.
     *
     * @param status The status value to set
     */
    public void setStatus(int status) {
        this.status = status;
    }


    /**
     * Get the status message.
     *
     * @return The message associated with the current status
     */
    public String getMessage() {
        return message;
    }


    /**
     * Set the status message.
     *
     * @param message The status message to set
     */
    public void setMessage(String message) {
        this.message = message;
    }


    public boolean isCommitted() {
        return committed;
    }


    public void setCommitted(boolean v) {
        if (v && !this.committed) {
            this.commitTimeNanos = System.nanoTime();
        }
        this.committed = v;
    }

    /**
     * Return the time the response was committed (based on System.currentTimeMillis).
     *
     * @return the time the response was committed
     */
    public long getCommitTime() {
        return System.currentTimeMillis() - TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - commitTimeNanos);
    }

    /**
     * Return the time the response was committed (based on System.nanoTime).
     *
     * @return the time the response was committed
     */
    public long getCommitTimeNanos() {
        return commitTimeNanos;
    }

    // -----------------Error State --------------------

    /**
     * Set the error Exception that occurred during the writing of the response processing.
     *
     * @param ex The exception that occurred
     */
    public void setErrorException(Exception ex) {
        if (errorException == null) {
            errorException = ex;
        }
    }


    /**
     * Get the Exception that occurred during the writing of the response.
     *
     * @return The exception that occurred
     */
    public Exception getErrorException() {
        return errorException;
    }


    public boolean isExceptionPresent() {
        return errorException != null;
    }


    /**
     * Set the error flag if not already set.
     */
    public void setError() {
        errorState.compareAndSet(0, 1);
    }


    /**
     * Error flag accessor.
     *
     * @return <code>true</code> if the response has encountered an error
     */
    public boolean isError() {
        return errorState.get() > 0;
    }


    public boolean isErrorReportRequired() {
        return errorState.get() == 1;
    }


    public boolean setErrorReported() {
        return errorState.compareAndSet(1, 2);
    }


    public void resetError() {
        errorState.set(0);
    }


    // -------------------- Methods --------------------

    public void reset() throws IllegalStateException {

        if (committed) {
            throw new IllegalStateException();
        }

        recycle(false);
    }


    // -------------------- Headers --------------------
    /**
     * Does the response contain the given header. <br>
     * Warning: This method always returns <code>false</code> for Content-Type and Content-Length.
     *
     * @param name The name of the header of interest
     *
     * @return {@code true} if the response contains the header.
     */
    public boolean containsHeader(String name) {
        return headers.getHeader(name) != null;
    }


    public void setHeader(String name, String value) {
        char cc = name.charAt(0);
        if (cc == 'C' || cc == 'c') {
            if (checkSpecialHeader(name, value)) {
                return;
            }
        }
        if (value == null) {
            headers.removeHeader(name);
        } else {
            headers.setValue(name).setString(value);
        }
    }


    public void addHeader(String name, String value) {
        addHeader(name, value, null);
    }


    public void addHeader(String name, String value, Charset charset) {
        char cc = name.charAt(0);
        if (cc == 'C' || cc == 'c') {
            if (checkSpecialHeader(name, value)) {
                return;
            }
        }
        MessageBytes mb = headers.addValue(name);
        if (charset != null) {
            mb.setCharset(charset);
        }
        mb.setString(value);
    }


    public void setTrailerFields(Supplier<Map<String,String>> supplier) {
        AtomicBoolean trailerFieldsSupported = new AtomicBoolean(false);
        action(ActionCode.IS_TRAILER_FIELDS_SUPPORTED, trailerFieldsSupported);
        if (!trailerFieldsSupported.get()) {
            throw new IllegalStateException(sm.getString("response.noTrailers.notSupported"));
        }

        this.trailerFieldsSupplier = supplier;
    }


    public Supplier<Map<String,String>> getTrailerFields() {
        return trailerFieldsSupplier;
    }


    /**
     * Set internal fields for special header names. Called from set/addHeader. Return true if the header is special, no
     * need to set the header.
     */
    private boolean checkSpecialHeader(String name, String value) {
        // XXX Eliminate redundant fields !!!
        // ( both header and in special fields )
        if (name.equalsIgnoreCase("Content-Type")) {
            setContentType(value);
            return true;
        }
        if (name.equalsIgnoreCase("Content-Length")) {
            try {
                if (value == null) {
                    setContentLength(-1);
                    return true;
                }
                long cL = Long.parseLong(value);
                setContentLength(cL);
                return true;
            } catch (NumberFormatException ex) {
                // Do nothing - the spec doesn't have any "throws"
                // and the user might know what they're doing
                return false;
            }
        }
        return false;
    }


    /**
     * Signal that we're done with the headers, and body will follow.
     */
    public void commit() {
        action(ActionCode.COMMIT, this);
        setCommitted(true);
    }


    // -------------------- I18N --------------------


    public Locale getLocale() {
        return locale;
    }

    /**
     * Called explicitly by user to set the Content-Language and the default encoding.
     *
     * @param locale The locale to use for this response
     */
    public void setLocale(Locale locale) {

        if (locale == null) {
            this.locale = null;
            this.contentLanguage = null;
            return;
        }

        // Save the locale for use by getLocale()
        this.locale = locale;

        // Set the contentLanguage for header output
        contentLanguage = locale.toLanguageTag();
    }

    /**
     * Return the content language.
     *
     * @return The language code for the language currently associated with this response
     */
    public String getContentLanguage() {
        return contentLanguage;
    }


    /**
     * Overrides the character encoding used in the body of the response. This method must be called prior to writing
     * output using getWriter().
     *
     * @param characterEncoding The name of character encoding.
     *
     * @throws UnsupportedEncodingException If the specified name is not recognised
     *
     * @deprecated Unused. Will be removed in Tomcat 12.
     */
    @Deprecated
    public void setCharacterEncoding(String characterEncoding) throws UnsupportedEncodingException {
        if (isCommitted()) {
            return;
        }

        charsetHolder = CharsetHolder.getInstance(characterEncoding);
        charsetHolder.validate();
    }


    /**
     * Returns the current character set.
     *
     * @return The current character set
     *
     * @deprecated Unused. Will be removed in Tomcat 12.
     */
    @Deprecated
    public Charset getCharset() {
        return charsetHolder.getCharset();
    }


    /**
     * Returns the name of the current encoding.
     *
     * @return The name of the current encoding
     *
     * @deprecated Unused. Will be removed in Tomcat 12.
     */
    @Deprecated
    public String getCharacterEncoding() {
        return charsetHolder.getName();
    }


    public CharsetHolder getCharsetHolder() {
        return charsetHolder;
    }


    public void setCharsetHolder(CharsetHolder charsetHolder) {
        this.charsetHolder = charsetHolder;
    }


    /**
     * Sets the content type. This method must preserve any response charset that may already have been set via a call
     * to response.setContentType(), response.setLocale(), or response.setCharacterEncoding().
     *
     * @param type the content type
     */
    public void setContentType(String type) {

        if (type == null) {
            this.contentType = null;
            return;
        }

        MediaType m = null;
        try {
            m = MediaType.parseMediaType(new StringReader(type));
        } catch (IOException ignore) {
            // Ignore - null test below handles this
        }
        if (m == null) {
            // Invalid - Assume no charset and just pass through whatever
            // the user provided.
            this.contentType = type;
            return;
        }

        String charsetValue = m.getCharset();

        if (charsetValue == null) {
            // No charset and we know value is valid as parser was successful
            // Pass-through user provided value in case user-agent is buggy and
            // requires specific format
            this.contentType = type;
        } else {
            // There is a charset so have to rebuild content-type without it
            this.contentType = m.toStringNoCharset();
            charsetValue = charsetValue.trim();
            if (!charsetValue.isEmpty()) {
                charsetHolder = CharsetHolder.getInstance(charsetValue);
                try {
                    charsetHolder.validate();
                } catch (UnsupportedEncodingException e) {
                    log.warn(sm.getString("response.encoding.invalid", charsetValue), e);
                }
            }
        }
    }

    public void setContentTypeNoCharset(String type) {
        this.contentType = type;
    }

    public String getContentType() {

        String ret = contentType;
        if (ret != null) {
            String charsetName = charsetHolder.getName();
            if (charsetName != null) {
                ret = ret + ";charset=" + charsetName;
            }
        }

        return ret;
    }

    public void setContentLength(long contentLength) {
        this.contentLength = contentLength;
    }

    public int getContentLength() {
        long length = getContentLengthLong();

        if (length < Integer.MAX_VALUE) {
            return (int) length;
        }
        return -1;
    }

    public long getContentLengthLong() {
        return contentLength;
    }


    /**
     * Write a chunk of bytes.
     *
     * @param chunk The ByteBuffer to write
     *
     * @throws IOException If an I/O error occurs during the write
     */
    public void doWrite(ByteBuffer chunk) throws IOException {
        int len = chunk.remaining();
        outputBuffer.doWrite(chunk);
        contentWritten += len - chunk.remaining();
    }


    public void recycle() {
        recycle(true);
    }


    private void recycle(boolean responseComplete) {
        contentType = null;
        contentLanguage = null;
        locale = DEFAULT_LOCALE;
        charsetHolder = CharsetHolder.EMPTY;
        contentLength = -1;
        status = 200;
        message = null;
        committed = false;
        commitTimeNanos = -1;
        errorException = null;
        resetError();
        headers.recycle();
        trailerFieldsSupplier = null;
        // Servlet 3.1 non-blocking write listener
        listener = null;
        synchronized (nonBlockingStateLock) {
            fireListener = false;
            registeredForWrite = false;
        }

        // update counters
        contentWritten = 0;

        if (responseComplete && hook instanceof NonPipeliningProcessor) {
            /*
             * No requirement to maintain state between responses so clear the hook (a.k.a. Processor) and the output
             * buffer to aid GC.
             *
             * Only clear these between responses. They need to be retained when the response is reset.
             */
            setHook(null);
            setOutputBuffer(null);
        }
    }


    /**
     * Bytes written by application - i.e. before compression, chunking, etc.
     *
     * @return The total number of bytes written to the response by the application. This will not be the number of
     *             bytes written to the network which may be more or less than this value.
     */
    public long getContentWritten() {
        return contentWritten;
    }

    /**
     * Bytes written to socket - i.e. after compression, chunking, etc.
     *
     * @param flush Should any remaining bytes be flushed before returning the total? If {@code false} bytes remaining
     *                  in the buffer will not be included in the returned value
     *
     * @return The total number of bytes written to the socket for this response
     */
    public long getBytesWritten(boolean flush) {
        if (flush) {
            action(ActionCode.CLIENT_FLUSH, this);
        }
        return outputBuffer.getBytesWritten();
    }

    /*
     * State for non-blocking output is maintained here as it is the one point easily reachable from the
     * CoyoteOutputStream and the Processor which both need access to state.
     */
    volatile WriteListener listener;
    // Ensures listener is only fired after a call is isReady()
    private boolean fireListener = false;
    // Tracks write registration to prevent duplicate registrations
    private boolean registeredForWrite = false;
    // Lock used to manage concurrent access to above flags
    private final Object nonBlockingStateLock = new Object();

    public WriteListener getWriteListener() {
        return listener;
    }

    public void setWriteListener(WriteListener listener) {
        if (listener == null) {
            throw new NullPointerException(sm.getString("response.nullWriteListener"));
        }
        if (getWriteListener() != null) {
            throw new IllegalStateException(sm.getString("response.writeListenerSet"));
        }
        // Note: This class is not used for HTTP upgrade so only need to test
        // for async
        AtomicBoolean result = new AtomicBoolean(false);
        action(ActionCode.ASYNC_IS_ASYNC, result);
        if (!result.get()) {
            throw new IllegalStateException(sm.getString("response.notAsync"));
        }

        this.listener = listener;

        // The container is responsible for the first call to
        // listener.onWritePossible(). If isReady() returns true, the container
        // needs to call listener.onWritePossible() from a new thread. If
        // isReady() returns false, the socket will be registered for write and
        // the container will call listener.onWritePossible() once data can be
        // written.
        if (isReady()) {
            synchronized (nonBlockingStateLock) {
                // Ensure we don't get multiple write registrations if
                // ServletOutputStream.isReady() returns false during a call to
                // onDataAvailable()
                registeredForWrite = true;
                // Need to set the fireListener flag otherwise when the
                // container tries to trigger onWritePossible, nothing will
                // happen
                fireListener = true;
            }
            action(ActionCode.DISPATCH_WRITE, null);
            if (!req.isRequestThread()) {
                // Not on a container thread so need to execute the dispatch
                action(ActionCode.DISPATCH_EXECUTE, null);
            }
        }
    }

    public boolean isReady() {
        if (listener == null) {
            return true;
        }
        // Assume write is not possible
        boolean ready;
        synchronized (nonBlockingStateLock) {
            if (registeredForWrite) {
                fireListener = true;
                return false;
            }
            ready = checkRegisterForWrite();
            fireListener = !ready;
        }
        return ready;
    }

    public boolean checkRegisterForWrite() {
        AtomicBoolean ready = new AtomicBoolean(false);
        synchronized (nonBlockingStateLock) {
            if (!registeredForWrite) {
                action(ActionCode.NB_WRITE_INTEREST, ready);
                registeredForWrite = !ready.get();
            }
        }
        return ready.get();
    }

    public void onWritePossible() throws IOException {
        // Any buffered data left over from a previous non-blocking write is
        // written in the Processor so if this point is reached the app is able
        // to write data.
        boolean fire = false;
        synchronized (nonBlockingStateLock) {
            registeredForWrite = false;
            if (fireListener) {
                fireListener = false;
                fire = true;
            }
        }
        if (fire) {
            listener.onWritePossible();
        }
    }
}
