/**
 * 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.router.sbbi;

import java.io.IOException;
import java.net.URL;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.SortedSet;
import java.util.TreeSet;

import org.chris.portmapper.PortMapperApp;
import org.chris.portmapper.Settings;
import org.chris.portmapper.model.PortMapping;
import org.chris.portmapper.model.Protocol;
import org.chris.portmapper.router.AbstractRouter;
import org.chris.portmapper.router.RouterException;
import org.chris.portmapper.util.EncodingUtilities;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import net.sbbi.upnp.devices.UPNPRootDevice;
import net.sbbi.upnp.impls.InternetGatewayDevice;
import net.sbbi.upnp.messages.UPNPResponseException;

/**
 * This class represents a router device and provides methods for managing port mappings and getting information about
 * the router. It useses the SBBI library's {@link InternetGatewayDevice}.
 */
public class SBBIRouter extends AbstractRouter {

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

    /**
     * The wrapped router device.
     */
    final private InternetGatewayDevice router;

    /**
     * The maximum number of port mappings that we will try to retrieve from the router.
     */
    private final static int MAX_NUM_PORTMAPPINGS = 500;

    private final PortMapperApp app;

    SBBIRouter(final PortMapperApp app, final InternetGatewayDevice router) {
        super(router.getIGDRootDevice().getModelName());
        this.app = app;
        this.router = router;
    }

    @Override
    public String getExternalIPAddress() throws RouterException {
        logger.debug("Get external IP address...");
        String ipAddress;
        try {
            ipAddress = router.getExternalIPAddress();
        } catch (final UPNPResponseException e) {
            throw new RouterException("Could not get external IP", e);
        } catch (final IOException e) {
            throw new RouterException("Could not get external IP", e);
        }
        logger.info("Got external IP address " + ipAddress + " for router.");
        return ipAddress;
    }

    @Override
    public String getInternalHostName() {
        logger.debug("Get internal IP address...");
        final URL presentationURL = router.getIGDRootDevice().getPresentationURL();
        if (presentationURL == null) {
            logger.warn("Did not get presentation url");
            return null;
        }
        final String ipAddress = presentationURL.getHost();
        logger.info("Got internal host name '{}' for router.", ipAddress);
        return ipAddress;
    }

    @Override
    public int getInternalPort() {
        logger.debug("Get internal port of router...");
        final URL presentationURL = router.getIGDRootDevice().getPresentationURL();
        // Presentation URL may be null in some situations.
        if (presentationURL != null) {
            final int presentationUrlPort = presentationURL.getPort();
            // https://sourceforge.net/tracker/?func=detail&aid=3198378&group_id=213879&atid=1027466
            // Some routers send an invalid presentationURL, in this case use
            // URLBase.
            if (presentationUrlPort > 0) {
                logger.debug("Got valid internal port {} from presentation URL '{}'", presentationUrlPort,
                        presentationURL);
                return presentationUrlPort;
            } else {
                logger.debug("Got invalid port {} from presentation url '{}'", presentationUrlPort, presentationURL);
            }
        } else {
            logger.debug("Presentation url is null");
        }
        final URL urlBase = router.getIGDRootDevice().getURLBase();
        final int urlBasePort = urlBase.getPort();
        logger.debug("Presentation URL is null or returns invalid port: using port {} of base url '{}'", urlBasePort,
                urlBase);

        return urlBasePort;
    }

    @Override
    public Collection<PortMapping> getPortMappings() throws RouterException {
        return new SBBIPortMappingExtractor(router, MAX_NUM_PORTMAPPINGS).getPortMappings();
    }

    @Override
    public void logRouterInfo() throws RouterException {
        final Map<String, String> info = new HashMap<>();
        final UPNPRootDevice rootDevice = router.getIGDRootDevice();
        info.put("friendlyName", rootDevice.getFriendlyName());
        info.put("manufacturer", rootDevice.getManufacturer());
        info.put("modelDescription", rootDevice.getModelDescription());
        info.put("modelName", rootDevice.getModelName());
        info.put("serialNumber", rootDevice.getSerialNumber());
        info.put("vendorFirmware", rootDevice.getVendorFirmware());

        info.put("modelNumber", rootDevice.getModelNumber());
        info.put("modelURL", rootDevice.getModelURL());
        info.put("manufacturerURL", rootDevice.getManufacturerURL().toExternalForm());
        info.put("presentationURL",
                rootDevice.getPresentationURL() != null ? rootDevice.getPresentationURL().toExternalForm() : null);
        info.put("urlBase", rootDevice.getURLBase().toExternalForm());

        final SortedSet<String> sortedKeys = new TreeSet<>(info.keySet());

        for (final String key : sortedKeys) {
            final String value = info.get(key);
            logger.info("Router Info: {} \t= {}", key, value);
        }

        logger.info("def loc: {}", rootDevice.getDeviceDefLoc());
        logger.trace("def loc data: {}", rootDevice.getDeviceDefLocData());
        logger.info("icons: {}", rootDevice.getDeviceIcons());
        logger.info("device type: {}", rootDevice.getDeviceType());
        logger.info("direct parent: {}", rootDevice.getDirectParent());
        logger.info("disc udn: {}", rootDevice.getDiscoveryUDN());
        logger.info("disc usn: {}", rootDevice.getDiscoveryUSN());
        logger.info("udn: {}", rootDevice.getUDN());
    }

    private boolean addPortMapping(final String description, final Protocol protocol, final String remoteHost,
            final int externalPort, final String internalClient, final int internalPort, final int leaseDuration)
                    throws RouterException {

        final String protocolString = protocol == Protocol.TCP ? "TCP" : "UDP";

        final String encodedDescription = encodeIfNecessary(description);

        try {
            final boolean success = router.addPortMapping(encodedDescription, null, internalPort, externalPort,
                    internalClient, leaseDuration, protocolString);
            return success;
        } catch (final IOException e) {
            throw new RouterException("Could not add port mapping: " + e.getMessage(), e);
        } catch (final UPNPResponseException e) {
            throw new RouterException("Could not add port mapping: " + e.getMessage(), e);
        }
    }

    private String encodeIfNecessary(final String description) {
        final Settings settings = app.getSettings();
        if (settings == null || settings.isUseEntityEncoding()) {
            return EncodingUtilities.htmlEntityEncode(description);
        }
        return description;
    }

    @Override
    public void addPortMappings(final Collection<PortMapping> mappings) throws RouterException {
        for (final PortMapping portMapping : mappings) {
            logger.info("Adding port mapping {}", portMapping);
            addPortMapping(portMapping);
        }
    }

    @Override
    public void addPortMapping(final PortMapping mapping) throws RouterException {
        logger.info("Adding port mapping ()", mapping.getCompleteDescription());
        addPortMapping(mapping.getDescription(), mapping.getProtocol(), mapping.getRemoteHost(),
                mapping.getExternalPort(), mapping.getInternalClient(), mapping.getInternalPort(), 0);
    }

    @Override
    public void removeMapping(final PortMapping mapping) throws RouterException {
        removePortMapping(mapping.getProtocol(), mapping.getRemoteHost(), mapping.getExternalPort());
    }

    @Override
    public void removePortMapping(final Protocol protocol, final String remoteHost, final int externalPort)
            throws RouterException {
        final String protocolString = (protocol.equals(Protocol.TCP) ? "TCP" : "UDP");
        try {
            router.deletePortMapping(remoteHost, externalPort, protocolString);
        } catch (final IOException e) {
            throw new RouterException("Could not remove port mapping", e);
        } catch (final UPNPResponseException e) {
            throw new RouterException("Could not remove port mapping", e);
        }
    }

    @Override
    public void disconnect() {
        // Nothing to do right now.
    }

    public long getUpTime() {
        // The SBBI library does not provide a method for getting the uptime.
        return 0;
    }
}
