/*
 * Copyright 2020 the original author or authors.
 *
 * Licensed 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
 *
 *      https://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.r2dbc.postgresql.client;

import io.r2dbc.postgresql.PostgresqlConnectionConfiguration;
import io.r2dbc.postgresql.message.backend.ErrorResponse;
import io.r2dbc.postgresql.message.backend.NoticeResponse;
import io.r2dbc.postgresql.util.Assert;
import io.r2dbc.postgresql.util.LogLevel;
import reactor.netty.resources.ConnectionProvider;
import reactor.netty.resources.LoopResources;
import reactor.util.annotation.Nullable;

import java.net.Socket;
import java.time.Duration;
import java.util.Collections;
import java.util.Map;
import java.util.function.Consumer;

/**
 * Value object capturing common connection settings.
 *
 * @since 0.9
 */
public final class ConnectionSettings {

    @Nullable
    private final Duration connectTimeout;

    private final ConnectionProvider connectionProvider;

    @Nullable
    private final LoopResources loopResources;

    private final SSLConfig sslConfig;

    private final Map<String, String> startupOptions;

    private final boolean tcpKeepAlive;

    private final boolean tcpNoDelay;

    private final LogLevel errorResponseLogLevel;

    private final LogLevel noticeLogLevel;

    ConnectionSettings(@Nullable Duration connectTimeout, ConnectionProvider connectionProvider, @Nullable LoopResources loopResources,
                       SSLConfig sslConfig, Map<String, String> startupOptions, boolean tcpKeepAlive, boolean tcpNoDelay, LogLevel errorResponseLogLevel, LogLevel noticeLogLevel) {
        this.connectTimeout = connectTimeout;
        this.connectionProvider = connectionProvider;
        this.loopResources = loopResources;
        this.sslConfig = sslConfig;
        this.startupOptions = startupOptions;
        this.tcpKeepAlive = tcpKeepAlive;
        this.tcpNoDelay = tcpNoDelay;
        this.errorResponseLogLevel = errorResponseLogLevel;
        this.noticeLogLevel = noticeLogLevel;
    }

    /**
     * Return a new {@link Builder}.
     *
     * @return a new {@link Builder}
     */
    public static Builder builder() {
        return new Builder();
    }

    /**
     * Return a {@link Builder} that is pre-initialized with the current values.
     *
     * @return a {@link Builder} that is pre-initialized with the current values
     */
    public Builder mutate() {
        return new Builder().connectionProvider(this.connectionProvider).loopResources(this.loopResources)
            .errorResponseLogLevel(this.errorResponseLogLevel).noticeLogLevel(this.noticeLogLevel).sslConfig(this.sslConfig)
            .connectTimeout(this.connectTimeout).startupOptions(this.startupOptions).tcpKeepAlive(this.tcpKeepAlive).tcpNoDelay(this.tcpNoDelay);
    }

    /**
     * Apply a {@link Consumer mutator} that creates a new {@link ConnectionSettings} object with the mutations applied.
     *
     * @return the mutated {@link ConnectionSettings}
     */
    public ConnectionSettings mutate(Consumer<Builder> mutator) {

        Builder builder = mutate();
        mutator.accept(builder);
        return builder.build();
    }

    boolean hasConnectionTimeout() {
        return this.connectTimeout != null;
    }

    int getConnectTimeoutMs() {

        if (this.connectTimeout == null) {
            throw new IllegalStateException("No connect timeout configured");
        }

        return Math.toIntExact(this.connectTimeout.toMillis());
    }

    ConnectionProvider getConnectionProvider() {
        return this.connectionProvider;
    }

    boolean hasLoopResources() {
        return this.loopResources != null;
    }

    LoopResources getRequiredLoopResources() {

        if (!hasLoopResources()) {
            throw new IllegalStateException("No LoopResources configured");
        }

        return this.loopResources;
    }

    SSLConfig getSslConfig() {
        return this.sslConfig;
    }

    Map<String, String> getStartupOptions() {
        return this.startupOptions;
    }

    boolean isTcpKeepAlive() {
        return this.tcpKeepAlive;
    }

    boolean isTcpNoDelay() {
        return this.tcpNoDelay;
    }

    LogLevel getErrorResponseLogLevel() {
        return this.errorResponseLogLevel;
    }

    LogLevel getNoticeLogLevel() {
        return this.noticeLogLevel;
    }

    /**
     * A builder for {@link ConnectionSettings} instances.
     * <p>
     * <i>This class is not threadsafe</i>
     */
    public static final class Builder {

        @Nullable
        private Duration connectTimeout;

        private ConnectionProvider connectionProvider = ConnectionProvider.newConnection();

        private LoopResources loopResources = null;

        private LogLevel errorResponseLogLevel = LogLevel.WARN;

        private LogLevel noticeLogLevel = LogLevel.DEBUG;

        private SSLConfig sslConfig = new SSLConfig(SSLMode.DISABLE, null, null);

        private Map<String, String> startupOptions = Collections.emptyMap();

        private boolean tcpKeepAlive;

        private boolean tcpNoDelay;

        private Builder() {
        }

        /**
         * Returns a configured {@link ConnectionSettings}.
         *
         * @return a configured {@link ConnectionSettings}
         */
        public ConnectionSettings build() {
            return new ConnectionSettings(this.connectTimeout, this.connectionProvider, this.loopResources, this.sslConfig,
                this.startupOptions, this.tcpKeepAlive, this.tcpNoDelay, this.errorResponseLogLevel, this.noticeLogLevel);
        }

        /**
         * Configure the connection timeout. Default unconfigured.
         *
         * @param connectTimeout the connection timeout
         * @return this {@link Builder}
         */
        public Builder connectTimeout(@Nullable Duration connectTimeout) {
            this.connectTimeout = connectTimeout;
            return this;
        }

        /**
         * Configure the {@link ConnectionProvider}.
         *
         * @param connectionProvider the connection provider
         * @return this {@link Builder}
         */
        public Builder connectionProvider(ConnectionProvider connectionProvider) {
            this.connectionProvider = Assert.requireNonNull(connectionProvider, "connectionProvider must not be null");
            return this;
        }

        /**
         * Configure the {@link LoopResources}.
         * The {@link LoopResources}'s lifecycle should be managed externally.
         *
         * @param loopResources the {@link LoopResources}.
         * @return this {@link Builder}
         * @since 0.8.5
         */
        public Builder loopResources(@Nullable LoopResources loopResources) {
            this.loopResources = loopResources;
            return this;
        }

        /**
         * Configure the {@link LogLevel} for {@link ErrorResponse error responses} that are part of a statement execution.
         *
         * @param errorResponseLogLevel the log level to use.
         * @return this {@link Builder}
         */
        public Builder errorResponseLogLevel(LogLevel errorResponseLogLevel) {
            this.errorResponseLogLevel = Assert.requireNonNull(errorResponseLogLevel, "errorResponseLogLevel must not be null");
            return this;
        }

        /**
         * Configure the {@link LogLevel} for {@link NoticeResponse notice responses}.
         *
         * @param noticeLogLevel the log level to use.
         * @return this {@link Builder}
         * @since 0.9
         */
        public Builder noticeLogLevel(LogLevel noticeLogLevel) {
            this.noticeLogLevel = Assert.requireNonNull(noticeLogLevel, "noticeLogLevel must not be null");
            return this;
        }

        /**
         * Configure the {@link SSLConfig SSL settings.}
         *
         * @param sslConfig the SSL configuration
         * @return this {@link Builder}
         */
        public Builder sslConfig(SSLConfig sslConfig) {
            this.sslConfig = Assert.requireNonNull(sslConfig, "sslConfig must not be null");
            return this;
        }

        /**
         * Configure the startup options.
         *
         * @param startupOptions the startup options
         * @return this {@link Builder}
         * @since 1.0
         */
        public Builder startupOptions(Map<String, String> startupOptions) {
            this.startupOptions = Assert.requireNonNull(startupOptions, "startupOptions must not be null");
            return this;
        }

        /**
         * Configure TCP KeepAlive.
         *
         * @param enabled whether to enable TCP KeepAlive
         * @return this {@link PostgresqlConnectionConfiguration.Builder}
         * @see Socket#setKeepAlive(boolean)
         */
        public Builder tcpKeepAlive(boolean enabled) {
            this.tcpKeepAlive = enabled;
            return this;
        }

        /**
         * Configure TCP NoDelay.
         *
         * @param enabled whether to enable TCP NoDelay
         * @return this {@link PostgresqlConnectionConfiguration.Builder}
         * @see Socket#setTcpNoDelay(boolean)
         */
        public Builder tcpNoDelay(boolean enabled) {
            this.tcpNoDelay = enabled;
            return this;
        }

    }

}
