/**
 * UPnP PortMapper - A tool for managing port forwardings via UPnP
 * Copyright (C) 2015 Christoph Pirkl <christoph at users.sourceforge.net>
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package org.chris.portmapper;

import java.lang.reflect.Constructor;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;

import org.chris.portmapper.model.PortMapping;
import org.chris.portmapper.model.Protocol;
import org.chris.portmapper.router.AbstractRouterFactory;
import org.chris.portmapper.router.IRouter;
import org.chris.portmapper.router.RouterException;
import org.chris.portmapper.router.cling.ClingRouterFactory;
import org.jdesktop.application.Application;
import org.jdesktop.application.utils.AppHelper;
import org.jdesktop.application.utils.PlatformType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class PortMapperCli {

    private final Logger logger = LoggerFactory.getLogger(this.getClass());

    private final CommandLineArguments cmdLineArgs;
    private String routerFactoryClassName = ClingRouterFactory.class.getName();
    private Integer routerIndex = null;

    public PortMapperCli() {
        cmdLineArgs = new CommandLineArguments();
    }

    public void start(final String[] args) {
        if (!cmdLineArgs.parse(args)) {
            System.exit(1);
        }
        if (isStartGuiRequired()) {
            startGui(args);
            return;
        }

        if (cmdLineArgs.getUpnpLib() != null) {
            this.routerFactoryClassName = cmdLineArgs.getUpnpLib();
            logger.info("Using router factory class '" + this.routerFactoryClassName + "'");
        }

        if (cmdLineArgs.getRouterIndex() != null) {
            this.routerIndex = cmdLineArgs.getRouterIndex();
            logger.info("Using router index " + this.routerIndex);
        }

        if (cmdLineArgs.isPrintHelp()) {
            printHelp();
            return;
        }
        try {
            final IRouter router = connect();
            if (router == null) {
                logger.error("No router found: exit");
                System.exit(1);
                return;
            }
            if (cmdLineArgs.isAddPortMapping()) {
                addPortForwarding(router);
            } else if (cmdLineArgs.isPrintInfo()) {
                printStatus(router);
            } else if (cmdLineArgs.isDeletePortMapping()) {
                deletePortForwardings(router);
            } else if (cmdLineArgs.isListPortMappings()) {
                printPortForwardings(router);
            } else {
                router.disconnect();
                System.err.println("Incorrect usage");
                printHelp();
                System.exit(1);
                return;
            }
            router.disconnect();
        } catch (final RouterException e) {
            logger.error("An error occured", e);
            System.exit(1);
            return;
        }
        System.exit(0);
    }

    private void startGui(final String[] args) {
        if (AppHelper.getPlatform() == PlatformType.OS_X) {
            MacSetup.setupMac();
        }
        Application.launch(PortMapperApp.class, args);
    }

    private void printPortForwardings(final IRouter router) throws RouterException {
        final Collection<PortMapping> mappings = router.getPortMappings();
        if (mappings.size() == 0) {
            logger.info("No port mappings found");
            return;
        }
        final StringBuilder b = new StringBuilder();
        for (final Iterator<PortMapping> iterator = mappings.iterator(); iterator.hasNext();) {
            final PortMapping mapping = iterator.next();
            b.append(mapping.getCompleteDescription());
            if (iterator.hasNext()) {
                b.append("\n");
            }
        }
        logger.info("Found " + mappings.size() + " port forwardings:\n" + b.toString());
    }

    private void deletePortForwardings(final IRouter router) throws RouterException {

        final String remoteHost = null;
        final int port = cmdLineArgs.getExternalPort();
        final Protocol protocol = cmdLineArgs.getProtocol();
        logger.info("Deleting mapping for protocol " + protocol + " and external port " + port);
        router.removePortMapping(protocol, remoteHost, port);
        printPortForwardings(router);
    }

    private void printStatus(final IRouter router) throws RouterException {
        router.logRouterInfo();
    }

    private void addPortForwarding(final IRouter router) throws RouterException {

        final String remoteHost = null;
        final String internalClient = cmdLineArgs.getInternalIp() != null ? cmdLineArgs.getInternalIp()
                : router.getLocalHostAddress();
        final int internalPort = cmdLineArgs.getInternalPort();
        final int externalPort = cmdLineArgs.getExternalPort();
        final Protocol protocol = cmdLineArgs.getProtocol();

        final String description = cmdLineArgs.getDescription() != null ? cmdLineArgs.getDescription()
                : "PortMapper " + protocol + "/" + internalClient + ":" + internalPort;
        final PortMapping mapping = new PortMapping(protocol, remoteHost, externalPort, internalClient, internalPort,
                description);
        logger.info("Adding mapping " + mapping);
        router.addPortMapping(mapping);
        printPortForwardings(router);
    }

    private void printHelp() {
        cmdLineArgs.printHelp();
    }

    private boolean isStartGuiRequired() {
        if (cmdLineArgs.isStartGui()) {
            return true;
        }
        return !(cmdLineArgs.isPrintHelp() || cmdLineArgs.isAddPortMapping() || cmdLineArgs.isPrintInfo()
                || cmdLineArgs.isListPortMappings() || cmdLineArgs.isDeletePortMapping());
    }

    @SuppressWarnings("unchecked")
    private AbstractRouterFactory createRouterFactory() throws RouterException {
        Class<AbstractRouterFactory> routerFactoryClass;
        logger.info("Creating router factory for class {}", routerFactoryClassName);
        try {
            routerFactoryClass = (Class<AbstractRouterFactory>) Class.forName(routerFactoryClassName);
        } catch (final ClassNotFoundException e) {
            throw new RouterException("Did not find router factory class for name " + routerFactoryClassName, e);
        }

        logger.debug("Creating a new instance of the router factory class {}", routerFactoryClass);
        try {
            final Constructor<AbstractRouterFactory> constructor = routerFactoryClass
                    .getConstructor(PortMapperApp.class);
            return constructor.newInstance(new PortMapperApp());
        } catch (final Exception e) {
            throw new RouterException("Error creating a router factory using class " + routerFactoryClass.getName(), e);
        }
    }

    private IRouter connect() throws RouterException {
        AbstractRouterFactory routerFactory;
        try {
            routerFactory = createRouterFactory();
        } catch (final RouterException e) {
            logger.error("Could not create router factory", e);
            return null;
        }
        logger.info("Searching for routers...");

        final List<IRouter> foundRouters = routerFactory.findRouters();

        return selectRouter(foundRouters);
    }

    /**
     * @param foundRouters
     * @return
     */
    private IRouter selectRouter(final List<IRouter> foundRouters) {
        // One router found: use it.
        if (foundRouters.size() == 1) {
            final IRouter router = foundRouters.iterator().next();
            logger.info("Connected to router " + router.getName());
            return router;
        } else if (foundRouters.size() == 0) {
            logger.error("Found no router");
            return null;
        } else if (foundRouters.size() > 1 && routerIndex == null) {
            // let user choose which router to use.
            logger.error("Found more than one router. Use option -i <index>");

            int index = 0;
            for (final IRouter iRouter : foundRouters) {
                logger.error("- index " + index + ": " + iRouter.getName());
                index++;
            }
            return null;
        } else if (routerIndex >= 0 && routerIndex < foundRouters.size()) {
            final IRouter router = foundRouters.get(routerIndex);
            logger.info("Found more than one router, using " + router.getName());
            return router;
        } else {
            logger.error("Index must be between 0 and " + (foundRouters.size() - 1));
            return null;
        }
    }
}
