/*
 * Copyright (c) 2019 the Eclipse Milo Authors
 *
 * This program and the accompanying materials are made
 * available under the terms of the Eclipse Public License 2.0
 * which is available at https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 */

package org.eclipse.milo.opcua.stack.core;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;

import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.util.HashedWheelTimer;
import io.netty.util.Timeout;
import org.eclipse.milo.opcua.stack.core.util.ManifestUtil;
import org.jetbrains.annotations.NotNull;
import org.slf4j.LoggerFactory;

public final class Stack {

    public static final String VERSION =
        ManifestUtil.read("X-Stack-Version").orElse("dev");

    public static final String TCP_UASC_UABINARY_TRANSPORT_URI =
        "http://opcfoundation.org/UA-Profile/Transport/uatcp-uasc-uabinary";

    public static final String HTTPS_UABINARY_TRANSPORT_URI =
        "http://opcfoundation.org/UA-Profile/Transport/https-uabinary";

    public static final String HTTPS_UAXML_TRANSPORT_URI =
        "http://opcfoundation.org/UA-Profile/Transport/https-uasoapxml";

    public static final String HTTPS_UAJSON_TRANSPORT_URI =
        "http://opcfoundation.org/UA-Profile/Transport/https-uajson";

    public static final String WSS_UASC_UABINARY_TRANSPORT_URI =
        "http://opcfoundation.org/UA-Profile/Transport/wss-uasc-uabinary";

    public static final String WSS_UAJSON_TRANSPORT_URI =
        "http://opcfoundation.org/UA-Profile/Transport/wss-uajson";

    public static final String WSS_PROTOCOL_BINARY = "opcua+uacp";

    public static final String WSS_PROTOCOL_JSON = "opcua+uajson";

    public static final int DEFAULT_TCP_PORT = 12685;
    public static final int DEFAULT_HTTP_PORT = 8080;
    public static final int DEFAULT_HTTPS_PORT = 8443;

    private static NioEventLoopGroup EVENT_LOOP;
    private static ExecutorService EXECUTOR_SERVICE;
    private static ScheduledExecutorService SCHEDULED_EXECUTOR_SERVICE;
    private static HashedWheelTimer WHEEL_TIMER;

    /**
     * @return a shared {@link NioEventLoopGroup}.
     */
    public static synchronized NioEventLoopGroup sharedEventLoop() {
        if (EVENT_LOOP == null) {
            ThreadFactory threadFactory = new ThreadFactory() {
                private final AtomicLong threadNumber = new AtomicLong(0L);

                @Override
                public Thread newThread(@NotNull Runnable r) {
                    Thread thread = new Thread(r, "milo-netty-event-loop-" + threadNumber.getAndIncrement());
                    thread.setDaemon(true);
                    return thread;
                }
            };

            EVENT_LOOP = new NioEventLoopGroup(0, threadFactory);
        }

        return EVENT_LOOP;
    }

    /**
     * @return a shared {@link ExecutorService}.
     */
    public static synchronized ExecutorService sharedExecutor() {
        if (EXECUTOR_SERVICE == null) {
            ThreadFactory threadFactory = new ThreadFactory() {
                private final AtomicLong threadNumber = new AtomicLong(0L);

                @Override
                public Thread newThread(@NotNull Runnable r) {
                    Thread thread = new Thread(r, "milo-shared-thread-pool-" + threadNumber.getAndIncrement());
                    thread.setDaemon(true);
                    thread.setUncaughtExceptionHandler(
                        (t, e) ->
                            LoggerFactory.getLogger(Stack.class)
                                .error("Uncaught Exception on shared stack ExecutorService thread!", e)
                    );
                    return thread;
                }
            };

            EXECUTOR_SERVICE = new ThreadPoolExecutor(
                0, Integer.MAX_VALUE,
                60L, TimeUnit.SECONDS,
                new SynchronousQueue<>(),
                threadFactory
            ) {

                @Override
                protected void afterExecute(Runnable r, Throwable t) {
                    super.afterExecute(r, t);

                    if (t != null) {
                        LoggerFactory.getLogger(Stack.class)
                            .error("Uncaught Throwable on shared stack ExecutorService thread!", t);
                    }
                }
            };
        }

        return EXECUTOR_SERVICE;
    }

    /**
     * @return a shared {@link ScheduledExecutorService}.
     */
    public static synchronized ScheduledExecutorService sharedScheduledExecutor() {
        if (SCHEDULED_EXECUTOR_SERVICE == null) {
            ThreadFactory threadFactory = new ThreadFactory() {
                private final AtomicLong threadNumber = new AtomicLong(0L);

                @Override
                public Thread newThread(@NotNull Runnable r) {
                    Thread thread = new Thread(r, "milo-shared-scheduled-executor-" + threadNumber.getAndIncrement());
                    thread.setDaemon(true);
                    thread.setUncaughtExceptionHandler(
                        (t, e) ->
                            LoggerFactory.getLogger(Stack.class)
                                .error("Uncaught Exception on shared stack ScheduledExecutorService thread!", e)
                    );
                    return thread;
                }
            };

            ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(
                Runtime.getRuntime().availableProcessors(),
                threadFactory
            ) {

                @Override
                protected void afterExecute(Runnable r, Throwable t) {
                    super.afterExecute(r, t);

                    if (t != null) {
                        LoggerFactory.getLogger(Stack.class)
                            .error("Uncaught Throwable on shared stack ScheduledExecutorService thread!", t);
                    }
                }
            };

            executor.setExecuteExistingDelayedTasksAfterShutdownPolicy(false);

            SCHEDULED_EXECUTOR_SERVICE = executor;
        }

        return SCHEDULED_EXECUTOR_SERVICE;
    }

    /**
     * @return a shared {@link HashedWheelTimer}.
     */
    public static synchronized HashedWheelTimer sharedWheelTimer() {
        if (WHEEL_TIMER == null) {
            ThreadFactory threadFactory = r -> {
                Thread thread = new Thread(r, "milo-netty-wheel-timer");
                thread.setDaemon(true);
                return thread;
            };

            WHEEL_TIMER = new HashedWheelTimer(threadFactory);
        }

        return WHEEL_TIMER;
    }

    /**
     * Release shared resources, waiting at most 5 seconds for the {@link NioEventLoopGroup} to shutdown gracefully.
     */
    public static synchronized void releaseSharedResources() {
        releaseSharedResources(5, TimeUnit.SECONDS);
    }

    /**
     * Release shared resources, waiting at most the specified timeout for the {@link NioEventLoopGroup} to shutdown
     * gracefully.
     *
     * @param timeout the duration of the timeout.
     * @param unit    the unit of the timeout duration.
     */
    public static synchronized void releaseSharedResources(long timeout, TimeUnit unit) {
        if (EVENT_LOOP != null) {
            try {
                EVENT_LOOP.shutdownGracefully().await(timeout, unit);
            } catch (InterruptedException e) {
                LoggerFactory.getLogger(Stack.class)
                    .warn("Interrupted awaiting event loop shutdown.", e);
            }
            EVENT_LOOP = null;
        }

        if (SCHEDULED_EXECUTOR_SERVICE != null) {
            SCHEDULED_EXECUTOR_SERVICE.shutdown();
        }

        if (EXECUTOR_SERVICE != null) {
            EXECUTOR_SERVICE.shutdown();
        }

        if (SCHEDULED_EXECUTOR_SERVICE != null) {
            try {
                SCHEDULED_EXECUTOR_SERVICE.awaitTermination(timeout, unit);
            } catch (InterruptedException e) {
                LoggerFactory.getLogger(Stack.class)
                    .warn("Interrupted awaiting scheduled executor service shutdown.", e);
            }
            SCHEDULED_EXECUTOR_SERVICE = null;
        }

        if (EXECUTOR_SERVICE != null) {
            try {
                EXECUTOR_SERVICE.awaitTermination(timeout, unit);
            } catch (InterruptedException e) {
                LoggerFactory.getLogger(Stack.class)
                    .warn("Interrupted awaiting executor service shutdown.", e);
            }
            EXECUTOR_SERVICE = null;
        }

        if (WHEEL_TIMER != null) {
            WHEEL_TIMER.stop().forEach(Timeout::cancel);
            WHEEL_TIMER = null;
        }
    }

    public static final class ConnectionLimits {

        private ConnectionLimits() {}

        /**
         * Deadline, in milliseconds, that UA Hello message must be received within before the channel is closed.
         */
        public static volatile int HELLO_DEADLINE_MS = 10_000;

        /**
         * Allows rate limiting to be disabled stack-wide.
         */
        public static boolean RATE_LIMIT_ENABLED = true;

        /**
         * Maximum number of connect attempts per {@link #RATE_LIMIT_WINDOW_MS}.
         */
        public static int RATE_LIMIT_MAX_ATTEMPTS = 4;

        /**
         * The window of time over which connect attempts will be counted for rate limiting.
         */
        public static int RATE_LIMIT_WINDOW_MS = 1000;

        /**
         * The maximum number of connections allowed in total (any remote address, not including localhost).
         */
        public static int RATE_LIMIT_MAX_CONNECTIONS = 10_000;

        /**
         * The maximum number of connections allowed from any 1 remote address.
         */
        public static int RATE_LIMIT_MAX_CONNECTIONS_PER_ADDRESS = 100;

    }

}
