/*
 * Copyright 2015 The Netty Project
 *
 * The Netty Project 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 io.netty.handler.codec.http2;

import io.netty.handler.codec.http2.Http2HeadersEncoder.SensitivityDetector;
import io.netty.util.internal.UnstableApi;

import static io.netty.handler.codec.http2.Http2CodecUtil.DEFAULT_HEADER_LIST_SIZE;
import static io.netty.handler.codec.http2.Http2CodecUtil.DEFAULT_INITIAL_HUFFMAN_DECODE_CAPACITY;
import static io.netty.handler.codec.http2.Http2CodecUtil.DEFAULT_MAX_RESERVED_STREAMS;
import static io.netty.util.internal.ObjectUtil.checkNotNull;
import static io.netty.util.internal.ObjectUtil.checkPositive;
import static io.netty.util.internal.ObjectUtil.checkPositiveOrZero;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.concurrent.TimeUnit.SECONDS;

/**
 * Abstract base class which defines commonly used features required to build {@link Http2ConnectionHandler} instances.
 *
 * <h3>Three ways to build a {@link Http2ConnectionHandler}</h3>
 * <h4>Let the builder create a {@link Http2ConnectionHandler}</h4>
 * Simply call all the necessary setter methods, and then use {@link #build()} to build a new
 * {@link Http2ConnectionHandler}. Setting the following properties are prohibited because they are used for
 * other ways of building a {@link Http2ConnectionHandler}.
 * conflicts with this option:
 * <ul>
 *   <li>{@link #connection(Http2Connection)}</li>
 *   <li>{@link #codec(Http2ConnectionDecoder, Http2ConnectionEncoder)}</li>
 * </ul>
 *
 *
 * <h4>Let the builder use the {@link Http2ConnectionHandler} you specified</h4>
 * Call {@link #connection(Http2Connection)} to tell the builder that you want to build the handler from the
 * {@link Http2Connection} you specified. Setting the following properties are prohibited and thus will trigger
 * an {@link IllegalStateException} because they conflict with this option.
 * <ul>
 *   <li>{@link #server(boolean)}</li>
 *   <li>{@link #codec(Http2ConnectionDecoder, Http2ConnectionEncoder)}</li>
 * </ul>
 *
 * <h4>Let the builder use the {@link Http2ConnectionDecoder} and {@link Http2ConnectionEncoder} you specified</h4>
 * Call {@link #codec(Http2ConnectionDecoder, Http2ConnectionEncoder)} to tell the builder that you want to built the
 * handler from the {@link Http2ConnectionDecoder} and {@link Http2ConnectionEncoder} you specified. Setting the
 * following properties are prohibited and thus will trigger an {@link IllegalStateException} because they conflict
 * with this option:
 * <ul>
 *   <li>{@link #server(boolean)}</li>
 *   <li>{@link #connection(Http2Connection)}</li>
 *   <li>{@link #frameLogger(Http2FrameLogger)}</li>
 *   <li>{@link #headerSensitivityDetector(SensitivityDetector)}</li>
 *   <li>{@link #encoderEnforceMaxConcurrentStreams(boolean)}</li>
 *   <li>{@link #encoderIgnoreMaxHeaderListSize(boolean)}</li>
 *   <li>{@link #initialHuffmanDecodeCapacity(int)}</li>
 * </ul>
 *
 * <h3>Exposing necessary methods in a subclass</h3>
 * {@link #build()} method and all property access methods are {@code protected}. Choose the methods to expose to the
 * users of your builder implementation and make them {@code public}.
 *
 * @param <T> The type of handler created by this builder.
 * @param <B> The concrete type of this builder.
 */
@UnstableApi
public abstract class AbstractHttp2ConnectionHandlerBuilder<T extends Http2ConnectionHandler,
                                                            B extends AbstractHttp2ConnectionHandlerBuilder<T, B>> {

    private static final SensitivityDetector DEFAULT_HEADER_SENSITIVITY_DETECTOR = Http2HeadersEncoder.NEVER_SENSITIVE;

    // The properties that can always be set.
    private Http2Settings initialSettings = Http2Settings.defaultSettings();
    private Http2FrameListener frameListener;
    private long gracefulShutdownTimeoutMillis = Http2CodecUtil.DEFAULT_GRACEFUL_SHUTDOWN_TIMEOUT_MILLIS;

    // The property that will prohibit connection() and codec() if set by server(),
    // because this property is used only when this builder creates a Http2Connection.
    private Boolean isServer;
    private Integer maxReservedStreams;

    // The property that will prohibit server() and codec() if set by connection().
    private Http2Connection connection;

    // The properties that will prohibit server() and connection() if set by codec().
    private Http2ConnectionDecoder decoder;
    private Http2ConnectionEncoder encoder;

    // The properties that are:
    // * mutually exclusive against codec() and
    // * OK to use with server() and connection()
    private Boolean validateHeaders;
    private Http2FrameLogger frameLogger;
    private SensitivityDetector headerSensitivityDetector;
    private Boolean encoderEnforceMaxConcurrentStreams;
    private Boolean encoderIgnoreMaxHeaderListSize;
    private int initialHuffmanDecodeCapacity = DEFAULT_INITIAL_HUFFMAN_DECODE_CAPACITY;

    /**
     * Sets the {@link Http2Settings} to use for the initial connection settings exchange.
     */
    protected Http2Settings initialSettings() {
        return initialSettings;
    }

    /**
     * Sets the {@link Http2Settings} to use for the initial connection settings exchange.
     */
    protected B initialSettings(Http2Settings settings) {
        initialSettings = checkNotNull(settings, "settings");
        return self();
    }

    /**
     * Returns the listener of inbound frames.
     *
     * @return {@link Http2FrameListener} if set, or {@code null} if not set.
     */
    protected Http2FrameListener frameListener() {
        return frameListener;
    }

    /**
     * Sets the listener of inbound frames.
     * This listener will only be set if the decoder's listener is {@code null}.
     */
    protected B frameListener(Http2FrameListener frameListener) {
        this.frameListener = checkNotNull(frameListener, "frameListener");
        return self();
    }

    /**
     * Returns the graceful shutdown timeout of the {@link Http2Connection} in milliseconds. Returns -1 if the
     * timeout is indefinite.
     */
    protected long gracefulShutdownTimeoutMillis() {
        return gracefulShutdownTimeoutMillis;
    }

    /**
     * Sets the graceful shutdown timeout of the {@link Http2Connection} in milliseconds.
     */
    protected B gracefulShutdownTimeoutMillis(long gracefulShutdownTimeoutMillis) {
        if (gracefulShutdownTimeoutMillis < -1) {
            throw new IllegalArgumentException("gracefulShutdownTimeoutMillis: " + gracefulShutdownTimeoutMillis +
                                               " (expected: -1 for indefinite or >= 0)");
        }
        this.gracefulShutdownTimeoutMillis = gracefulShutdownTimeoutMillis;
        return self();
    }

    /**
     * Returns if {@link #build()} will to create a {@link Http2Connection} in server mode ({@code true})
     * or client mode ({@code false}).
     */
    protected boolean isServer() {
        return isServer != null ? isServer : true;
    }

    /**
     * Sets if {@link #build()} will to create a {@link Http2Connection} in server mode ({@code true})
     * or client mode ({@code false}).
     */
    protected B server(boolean isServer) {
        enforceConstraint("server", "connection", connection);
        enforceConstraint("server", "codec", decoder);
        enforceConstraint("server", "codec", encoder);

        this.isServer = isServer;
        return self();
    }

    /**
     * Get the maximum number of streams which can be in the reserved state at any given time.
     * <p>
     * By default this value will be ignored on the server for local endpoint. This is because the RFC provides
     * no way to explicitly communicate a limit to how many states can be in the reserved state, and instead relies
     * on the peer to send RST_STREAM frames when they will be rejected.
     */
    protected int maxReservedStreams() {
        return maxReservedStreams != null ? maxReservedStreams : DEFAULT_MAX_RESERVED_STREAMS;
    }

    /**
     * Set the maximum number of streams which can be in the reserved state at any given time.
     */
    protected B maxReservedStreams(int maxReservedStreams) {
        enforceConstraint("server", "connection", connection);
        enforceConstraint("server", "codec", decoder);
        enforceConstraint("server", "codec", encoder);

        this.maxReservedStreams = checkPositiveOrZero(maxReservedStreams, "maxReservedStreams");
        return self();
    }

    /**
     * Returns the {@link Http2Connection} to use.
     *
     * @return {@link Http2Connection} if set, or {@code null} if not set.
     */
    protected Http2Connection connection() {
        return connection;
    }

    /**
     * Sets the {@link Http2Connection} to use.
     */
    protected B connection(Http2Connection connection) {
        enforceConstraint("connection", "maxReservedStreams", maxReservedStreams);
        enforceConstraint("connection", "server", isServer);
        enforceConstraint("connection", "codec", decoder);
        enforceConstraint("connection", "codec", encoder);

        this.connection = checkNotNull(connection, "connection");

        return self();
    }

    /**
     * Returns the {@link Http2ConnectionDecoder} to use.
     *
     * @return {@link Http2ConnectionDecoder} if set, or {@code null} if not set.
     */
    protected Http2ConnectionDecoder decoder() {
        return decoder;
    }

    /**
     * Returns the {@link Http2ConnectionEncoder} to use.
     *
     * @return {@link Http2ConnectionEncoder} if set, or {@code null} if not set.
     */
    protected Http2ConnectionEncoder encoder() {
        return encoder;
    }

    /**
     * Sets the {@link Http2ConnectionDecoder} and {@link Http2ConnectionEncoder} to use.
     */
    protected B codec(Http2ConnectionDecoder decoder, Http2ConnectionEncoder encoder) {
        enforceConstraint("codec", "server", isServer);
        enforceConstraint("codec", "maxReservedStreams", maxReservedStreams);
        enforceConstraint("codec", "connection", connection);
        enforceConstraint("codec", "frameLogger", frameLogger);
        enforceConstraint("codec", "validateHeaders", validateHeaders);
        enforceConstraint("codec", "headerSensitivityDetector", headerSensitivityDetector);
        enforceConstraint("codec", "encoderEnforceMaxConcurrentStreams", encoderEnforceMaxConcurrentStreams);

        checkNotNull(decoder, "decoder");
        checkNotNull(encoder, "encoder");

        if (decoder.connection() != encoder.connection()) {
            throw new IllegalArgumentException("The specified encoder and decoder have different connections.");
        }

        this.decoder = decoder;
        this.encoder = encoder;

        return self();
    }

    /**
     * Returns if HTTP headers should be validated according to
     * <a href="https://tools.ietf.org/html/rfc7540#section-8.1.2.6">RFC 7540, 8.1.2.6</a>.
     */
    protected boolean isValidateHeaders() {
        return validateHeaders != null ? validateHeaders : true;
    }

    /**
     * Sets if HTTP headers should be validated according to
     * <a href="https://tools.ietf.org/html/rfc7540#section-8.1.2.6">RFC 7540, 8.1.2.6</a>.
     */
    protected B validateHeaders(boolean validateHeaders) {
        enforceNonCodecConstraints("validateHeaders");
        this.validateHeaders = validateHeaders;
        return self();
    }

    /**
     * Returns the logger that is used for the encoder and decoder.
     *
     * @return {@link Http2FrameLogger} if set, or {@code null} if not set.
     */
    protected Http2FrameLogger frameLogger() {
        return frameLogger;
    }

    /**
     * Sets the logger that is used for the encoder and decoder.
     */
    protected B frameLogger(Http2FrameLogger frameLogger) {
        enforceNonCodecConstraints("frameLogger");
        this.frameLogger = checkNotNull(frameLogger, "frameLogger");
        return self();
    }

    /**
     * Returns if the encoder should queue frames if the maximum number of concurrent streams
     * would otherwise be exceeded.
     */
    protected boolean encoderEnforceMaxConcurrentStreams() {
        return encoderEnforceMaxConcurrentStreams != null ? encoderEnforceMaxConcurrentStreams : false;
    }

    /**
     * Sets if the encoder should queue frames if the maximum number of concurrent streams
     * would otherwise be exceeded.
     */
    protected B encoderEnforceMaxConcurrentStreams(boolean encoderEnforceMaxConcurrentStreams) {
        enforceNonCodecConstraints("encoderEnforceMaxConcurrentStreams");
        this.encoderEnforceMaxConcurrentStreams = encoderEnforceMaxConcurrentStreams;
        return self();
    }

    /**
     * Returns the {@link SensitivityDetector} to use.
     */
    protected SensitivityDetector headerSensitivityDetector() {
        return headerSensitivityDetector != null ? headerSensitivityDetector : DEFAULT_HEADER_SENSITIVITY_DETECTOR;
    }

    /**
     * Sets the {@link SensitivityDetector} to use.
     */
    protected B headerSensitivityDetector(SensitivityDetector headerSensitivityDetector) {
        enforceNonCodecConstraints("headerSensitivityDetector");
        this.headerSensitivityDetector = checkNotNull(headerSensitivityDetector, "headerSensitivityDetector");
        return self();
    }

    /**
     * Sets if the <a href="https://tools.ietf.org/html/rfc7540#section-6.5.2">SETTINGS_MAX_HEADER_LIST_SIZE</a>
     * should be ignored when encoding headers.
     * @param ignoreMaxHeaderListSize {@code true} to ignore
     * <a href="https://tools.ietf.org/html/rfc7540#section-6.5.2">SETTINGS_MAX_HEADER_LIST_SIZE</a>.
     * @return this.
     */
    protected B encoderIgnoreMaxHeaderListSize(boolean ignoreMaxHeaderListSize) {
        enforceNonCodecConstraints("encoderIgnoreMaxHeaderListSize");
        this.encoderIgnoreMaxHeaderListSize = ignoreMaxHeaderListSize;
        return self();
    }

    /**
     * Sets the initial size of an intermediate buffer used during HPACK huffman decoding.
     * @param initialHuffmanDecodeCapacity initial size of an intermediate buffer used during HPACK huffman decoding.
     * @return this.
     */
    protected B initialHuffmanDecodeCapacity(int initialHuffmanDecodeCapacity) {
        enforceNonCodecConstraints("initialHuffmanDecodeCapacity");
        this.initialHuffmanDecodeCapacity = checkPositive(initialHuffmanDecodeCapacity, "initialHuffmanDecodeCapacity");
        return self();
    }

    /**
     * Create a new {@link Http2ConnectionHandler}.
     */
    protected T build() {
        if (encoder != null) {
            assert decoder != null;
            return buildFromCodec(decoder, encoder);
        }

        Http2Connection connection = this.connection;
        if (connection == null) {
            connection = new DefaultHttp2Connection(isServer(), maxReservedStreams());
        }

        return buildFromConnection(connection);
    }

    private T buildFromConnection(Http2Connection connection) {
        Long maxHeaderListSize = initialSettings.maxHeaderListSize();
        Http2FrameReader reader = new DefaultHttp2FrameReader(new DefaultHttp2HeadersDecoder(isValidateHeaders(),
                maxHeaderListSize == null ? DEFAULT_HEADER_LIST_SIZE : maxHeaderListSize,
                initialHuffmanDecodeCapacity));
        Http2FrameWriter writer = encoderIgnoreMaxHeaderListSize == null ?
                new DefaultHttp2FrameWriter(headerSensitivityDetector()) :
                new DefaultHttp2FrameWriter(headerSensitivityDetector(), encoderIgnoreMaxHeaderListSize);

        if (frameLogger != null) {
            reader = new Http2InboundFrameLogger(reader, frameLogger);
            writer = new Http2OutboundFrameLogger(writer, frameLogger);
        }

        Http2ConnectionEncoder encoder = new DefaultHttp2ConnectionEncoder(connection, writer);
        boolean encoderEnforceMaxConcurrentStreams = encoderEnforceMaxConcurrentStreams();

        if (encoderEnforceMaxConcurrentStreams) {
            if (connection.isServer()) {
                encoder.close();
                reader.close();
                throw new IllegalArgumentException(
                        "encoderEnforceMaxConcurrentStreams: " + encoderEnforceMaxConcurrentStreams +
                        " not supported for server");
            }
            encoder = new StreamBufferingEncoder(encoder);
        }

        Http2ConnectionDecoder decoder = new DefaultHttp2ConnectionDecoder(connection, encoder, reader);
        return buildFromCodec(decoder, encoder);
    }

    private T buildFromCodec(Http2ConnectionDecoder decoder, Http2ConnectionEncoder encoder) {
        final T handler;
        try {
            // Call the abstract build method
            handler = build(decoder, encoder, initialSettings);
        } catch (Throwable t) {
            encoder.close();
            decoder.close();
            throw new IllegalStateException("failed to build a Http2ConnectionHandler", t);
        }

        // Setup post build options
        handler.gracefulShutdownTimeoutMillis(gracefulShutdownTimeoutMillis);
        if (handler.decoder().frameListener() == null) {
            handler.decoder().frameListener(frameListener);
        }
        return handler;
    }

    /**
     * Implement this method to create a new {@link Http2ConnectionHandler} or its subtype instance.
     * <p>
     * The return of this method will be subject to the following:
     * <ul>
     *   <li>{@link #frameListener(Http2FrameListener)} will be set if not already set in the decoder</li>
     *   <li>{@link #gracefulShutdownTimeoutMillis(long)} will always be set</li>
     * </ul>
     */
    protected abstract T build(Http2ConnectionDecoder decoder, Http2ConnectionEncoder encoder,
                               Http2Settings initialSettings) throws Exception;

    /**
     * Returns {@code this}.
     */
    @SuppressWarnings("unchecked")
    protected final B self() {
        return (B) this;
    }

    private void enforceNonCodecConstraints(String rejected) {
        enforceConstraint(rejected, "server/connection", decoder);
        enforceConstraint(rejected, "server/connection", encoder);
    }

    private static void enforceConstraint(String methodName, String rejectorName, Object value) {
        if (value != null) {
            throw new IllegalStateException(
                    methodName + "() cannot be called because " + rejectorName + "() has been called already.");
        }
    }
}
