package com.basrikahveci.p2p;

import com.basrikahveci.p2p.peer.Config;
import com.google.common.base.Charsets;
import joptsimple.OptionParser;
import joptsimple.OptionSet;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Properties;

public class Main {

    private static final Logger LOGGER = LoggerFactory.getLogger(Main.class);

    private static final String PEER_NAME_SYSTEM_PROPERTY = "peerName";

    private static final String PEER_NAME_PARAMETER = "n";

    private static final String BIND_PORT_PARAMETER = "b";

    private static final String CONFIG_FILE_PARAMETER = "c";

    private static final String HELP_PARAMETER = "help";

    private enum ConfigProperty {

        MIN_NUMBER_OF_ACTIVE_CONNECTIONS("minActiveConnections") {
            @Override
            public void setIntValue(int val, Config config) {
                config.setMinNumberOfActiveConnections(val);
            }
        },

        MAX_READ_IDLE_SECONDS("maxReadIdleSeconds") {
            @Override
            public void setIntValue(int val, Config config) {
                config.setMaxReadIdleSeconds(val);
            }
        },

        KEEP_ALIVE_PING_PERIOD_SECONDS("keepAlivePingPeriodSeconds") {
            @Override
            public void setIntValue(int val, Config config) {
                config.setKeepAlivePeriodSeconds(val);
            }
        },

        PING_TIMEOUT_SECONDS("pingTimeoutSeconds") {
            @Override
            public void setIntValue(int val, Config config) {
                config.setPingTimeoutSeconds(val);
            }
        },

        AUTO_DISCOVERY_PING_FREQUENCY("autoDiscoveryPingFrequency") {
            @Override
            public void setIntValue(int val, Config config) {
                config.setAutoDiscoveryPingFrequency(val);
            }
        },

        PING_TTL("pingTTL") {
            @Override
            public void setIntValue(int val, Config config) {
                config.setPingTTL(val);
            }
        },

        LEADER_ELECTION_TIMEOUT("leaderElectionTimeoutSeconds") {
            @Override
            public void setIntValue(int val, Config config) {
                config.setLeaderElectionTimeoutSeconds(val);
            }
        },

        LEADER_REJECTION_TIMEOUT("leaderRejectionTimeoutSeconds") {
            @Override
            public void setIntValue(int val, Config config) {
                config.setLeaderRejectionTimeoutSeconds(val);
            }
        };

        public static ConfigProperty byPropertyName(final String propertyName) {
            for (ConfigProperty prop : values()) {
                if (prop.propertyName.equals(propertyName)) {
                    return prop;
                }
            }

            throw new IllegalArgumentException("Invalid config property: " + propertyName);
        }

        private final String propertyName;

        ConfigProperty(String propertyName) {
            this.propertyName = propertyName;
        }

        public abstract void setIntValue(final int val, final Config config);

    }

    public static void main(String[] args) throws IOException, InterruptedException {
        if (System.getProperty(PEER_NAME_SYSTEM_PROPERTY) == null) {
            LOGGER.error("System property peerName should be provided!");
            System.exit(-1);
        }

        final OptionSet options = parseArguments(args);
        final PeerRunner peerRunner = createPeerRunner(options);
        peerRunner.start();

        String line;
        final BufferedReader reader = new BufferedReader(new InputStreamReader(System.in, Charsets.UTF_8));
        while ((line = reader.readLine()) != null) {
            final PeerRunner.CommandResult result = peerRunner.handleCommand(line);
            if (result == PeerRunner.CommandResult.SHUT_DOWN) {
                break;
            } else if (result == PeerRunner.CommandResult.INVALID_COMMAND && line.length() > 0) {
                printHelp(line);
            }
        }
    }

    private static PeerRunner createPeerRunner(final OptionSet options) throws IOException, InterruptedException {
        final String peerName = (String) options.valueOf(PEER_NAME_PARAMETER);
        final int portToBind = (int) options.valueOf(BIND_PORT_PARAMETER);

        final Config config = new Config();
        config.setPeerName(peerName);
        populateConfig(options, config);

        return new PeerRunner(config, portToBind);
    }

    private static OptionSet parseArguments(final String[] args) throws IOException {
        final OptionParser optionParser = new OptionParser();
        optionParser.accepts(PEER_NAME_PARAMETER).withRequiredArg().ofType(String.class).describedAs("peer name");
        optionParser.accepts(BIND_PORT_PARAMETER).withRequiredArg().ofType(Integer.class).describedAs("port to bind");
        optionParser.accepts(CONFIG_FILE_PARAMETER).withOptionalArg().ofType(File.class).describedAs("config properties file");
        optionParser.accepts(HELP_PARAMETER).forHelp();

        final OptionSet options = optionParser.parse(args);
        if (options.has(HELP_PARAMETER)) {
            optionParser.printHelpOn(System.out);
        }

        if (!options.has(PEER_NAME_PARAMETER) || !options.has(BIND_PORT_PARAMETER)) {
            if (!options.has(HELP_PARAMETER)) {
                optionParser.printHelpOn(System.out);
            }
            LOGGER.error("Missing arguments!!");
            System.exit(-1);
        }

        return options;
    }

    private static void populateConfig(final OptionSet options, final Config config) throws IOException {
        if (options.has(CONFIG_FILE_PARAMETER)) {
            final File file = (File) options.valueOf(CONFIG_FILE_PARAMETER);
            loadConfig(config, file);
        }

        LOGGER.info("Using configuration: {}", config);
    }

    private static void loadConfig(final Config config, final File file) throws IOException {
        final Properties properties = new Properties();
        final FileInputStream fileInputStream = new FileInputStream(file);
        properties.load(fileInputStream);
        fileInputStream.close();
        for (String propertyName : properties.stringPropertyNames()) {
            final ConfigProperty configProperty = ConfigProperty.byPropertyName(propertyName);
            final int val = Integer.parseInt(properties.getProperty(propertyName));
            configProperty.setIntValue(val, config);
        }
    }

    private static void printHelp(final String line) {
        if (!"help".equalsIgnoreCase(line.trim())) {
            System.out.println("Invalid input command:  " + line);
        }

        System.out.println(
                "############################################## COMMANDS ###############################################");
        System.out.println(
                "# 1) ping                >>> Lists peers in the network                                               #");
        System.out.println(
                "# 2) leave               >>> Leaves the network                                                       #");
        System.out.println(
                "# 3) connect host port   >>> Connects to the peer specified by host:port pair                         #");
        System.out.println(
                "# 4) disconnect peerName >>> Disconnects from the peer specified with peerName                        #");
        System.out.println(
                "# 4) election            >>> Starts a new leader election                                             #");
        System.out.println(
                "#######################################################################################################");
    }


}