/*
 * 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.logging.log4j.core.net.server;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.InetAddress;
import java.net.URI;
import java.net.URL;
import java.util.Collections;
import java.util.List;
import java.util.Objects;

import com.beust.jcommander.Parameter;
import com.beust.jcommander.validators.PositiveInteger;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.core.LogEventListener;
import org.apache.logging.log4j.core.LoggerContext;
import org.apache.logging.log4j.core.config.Configuration;
import org.apache.logging.log4j.core.config.ConfigurationSource;
import org.apache.logging.log4j.core.config.xml.XmlConfiguration;
import org.apache.logging.log4j.core.config.xml.XmlConfigurationFactory;
import org.apache.logging.log4j.core.util.BasicCommandLineArguments;
import org.apache.logging.log4j.core.util.InetAddressConverter;
import org.apache.logging.log4j.core.util.Log4jThread;
import org.apache.logging.log4j.util.Strings;

/**
 * Abstract socket server for TCP and UDP implementations.
 *
 * @param <T>
 *            The kind of input stream read
 *
 *            TODO Make a LifeCycle
 */
public abstract class AbstractSocketServer<T extends InputStream> extends LogEventListener implements Runnable {

    protected static class CommandLineArguments extends BasicCommandLineArguments {

        @Parameter(names = { "--config", "-c" }, description = "Log4j configuration file location (path or URL).")
        private String configLocation;

        @Parameter(names = { "--interactive",
                "-i" }, description = "Accepts commands on standard input (\"exit\" is the only command).")
        private boolean interactive;

        @Parameter(names = { "--port",
                "-p" }, validateWith = PositiveInteger.class, description = "Server socket port.")
        private int port;

        @Parameter(names = { "--localbindaddress",
                "-a" }, converter = InetAddressConverter.class, description = "Server socket local bind address.")
        private InetAddress localBindAddress;

        @Parameter(names = {"--classes", "-C"}, description = "Additional classes to allow deserialization")
        private List<String> allowedClasses;

        String getConfigLocation() {
            return configLocation;
        }

        int getPort() {
            return port;
        }

        protected boolean isInteractive() {
            return interactive;
        }

        void setConfigLocation(final String configLocation) {
            this.configLocation = configLocation;
        }

        void setInteractive(final boolean interactive) {
            this.interactive = interactive;
        }

        void setPort(final int port) {
            this.port = port;
        }

        InetAddress getLocalBindAddress() {
            return localBindAddress;
        }

        void setLocalBindAddress(final InetAddress localBindAddress) {
            this.localBindAddress = localBindAddress;
        }

        List<String> getAllowedClasses() {
            return allowedClasses == null ? Collections.<String>emptyList() : allowedClasses;
        }

        void setAllowedClasses(final List<String> allowedClasses) {
            this.allowedClasses = allowedClasses;
        }
    }

    /**
     * Factory that creates a Configuration for the server.
     */
    protected static class ServerConfigurationFactory extends XmlConfigurationFactory {

        private final String path;

        public ServerConfigurationFactory(final String path) {
            this.path = path;
        }

        @Override
        public Configuration getConfiguration(final LoggerContext loggerContext, final String name,
                final URI configLocation) {
            if (Strings.isNotEmpty(path)) {
                File file = null;
                ConfigurationSource source = null;
                try {
                    file = new File(path);
                    final FileInputStream is = new FileInputStream(file);
                    source = new ConfigurationSource(is, file);
                } catch (final FileNotFoundException ignored) {
                    // Ignore this error
                }
                if (source == null) {
                    try {
                        final URL url = new URL(path);
                        source = new ConfigurationSource(url.openStream(), url);
                    } catch (final IOException ignored) {
                        // Ignore this error
                    }
                }

                try {
                    if (source != null) {
                        return new XmlConfiguration(loggerContext, source);
                    }
                } catch (final Exception ignored) {
                    // Ignore this error.
                }
                System.err.println("Unable to process configuration at " + path + ", using default.");
            }
            return super.getConfiguration(loggerContext, name, configLocation);
        }
    }

    protected static final int MAX_PORT = 65534;

    private volatile boolean active = true;

    protected final LogEventBridge<T> logEventInput;

    protected final Logger logger;

    /**
     * Creates a new socket server.
     *
     * @param port
     *            listen to this port
     * @param logEventInput
     *            Use this input to read log events.
     */
    public AbstractSocketServer(final int port, final LogEventBridge<T> logEventInput) {
        this.logger = LogManager.getLogger(this.getClass().getName() + '.' + port);
        this.logEventInput = Objects.requireNonNull(logEventInput, "LogEventInput");
    }

    protected boolean isActive() {
        return this.active;
    }

    protected void setActive(final boolean isActive) {
        this.active = isActive;
    }

    /**
     * Start this server in a new thread.
     *
     * @return the new thread that running this server.
     */
    public Thread startNewThread() {
        final Thread thread = new Log4jThread(this);
        thread.start();
        return thread;
    }

    public abstract void shutdown() throws Exception;

    public void awaitTermination(final Thread serverThread) throws Exception {
        final BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
        while (true) {
            final String line = reader.readLine();
            if (line == null
                || line.equalsIgnoreCase("quit")
                || line.equalsIgnoreCase("stop")
                || line.equalsIgnoreCase("exit")) {
                this.shutdown();
                serverThread.join();
                break;
            }
        }
    }

}
