/**
 * All rights reserved. Licensed 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.jivesoftware.smackx.bytestreams.socks5.packet;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;

import org.jivesoftware.smack.packet.IQ;
import org.jivesoftware.smack.packet.PacketExtension;

/**
 * A packet representing part of a SOCKS5 Bytestream negotiation.
 *
 * @author Alexander Wenckus
 */
public class Bytestream extends IQ {

    private String sessionID;

    private Mode mode = Mode.tcp;

    private final List<StreamHost> streamHosts = new ArrayList<StreamHost>();

    private StreamHostUsed usedHost;

    private Activate toActivate;

    /**
     * The default constructor
     */
    public Bytestream() {
        super();
    }

    /**
     * A constructor where the session ID can be specified.
     *
     * @param SID The session ID related to the negotiation.
     * @see #setSessionID(String)
     */
    public Bytestream(final String SID) {
        super();
        setSessionID(SID);
    }

    /**
     * Set the session ID related to the bytestream. The session ID is a unique identifier used to
     * differentiate between stream negotiations.
     *
     * @param sessionID the unique session ID that identifies the transfer.
     */
    public void setSessionID(final String sessionID) {
        this.sessionID = sessionID;
    }

    /**
     * Returns the session ID related to the bytestream negotiation.
     *
     * @return Returns the session ID related to the bytestream negotiation.
     * @see #setSessionID(String)
     */
    public String getSessionID() {
        return sessionID;
    }

    /**
     * Set the transport mode. This should be put in the initiation of the interaction.
     *
     * @param mode the transport mode, either UDP or TCP
     * @see Mode
     */
    public void setMode(final Mode mode) {
        this.mode = mode;
    }

    /**
     * Returns the transport mode.
     *
     * @return Returns the transport mode.
     * @see #setMode(Mode)
     */
    public Mode getMode() {
        return mode;
    }

    /**
     * Adds a potential stream host that the remote user can connect to to receive the file.
     *
     * @param JID     The JID of the stream host.
     * @param address The internet address of the stream host.
     * @return The added stream host.
     */
    public StreamHost addStreamHost(final String JID, final String address) {
        return addStreamHost(JID, address, 0);
    }

    /**
     * Adds a potential stream host that the remote user can connect to to receive the file.
     *
     * @param JID     The JID of the stream host.
     * @param address The internet address of the stream host.
     * @param port    The port on which the remote host is seeking connections.
     * @return The added stream host.
     */
    public StreamHost addStreamHost(final String JID, final String address, final int port) {
        StreamHost host = new StreamHost(JID, address);
        host.setPort(port);
        addStreamHost(host);

        return host;
    }

    /**
     * Adds a potential stream host that the remote user can transfer the file through.
     *
     * @param host The potential stream host.
     */
    public void addStreamHost(final StreamHost host) {
        streamHosts.add(host);
    }

    /**
     * Returns the list of stream hosts contained in the packet.
     *
     * @return Returns the list of stream hosts contained in the packet.
     */
    public Collection<StreamHost> getStreamHosts() {
        return Collections.unmodifiableCollection(streamHosts);
    }

    /**
     * Returns the stream host related to the given JID, or null if there is none.
     *
     * @param JID The JID of the desired stream host.
     * @return Returns the stream host related to the given JID, or null if there is none.
     */
    public StreamHost getStreamHost(final String JID) {
        if (JID == null) {
            return null;
        }
        for (StreamHost host : streamHosts) {
            if (host.getJID().equals(JID)) {
                return host;
            }
        }

        return null;
    }

    /**
     * Returns the count of stream hosts contained in this packet.
     *
     * @return Returns the count of stream hosts contained in this packet.
     */
    public int countStreamHosts() {
        return streamHosts.size();
    }

    /**
     * Upon connecting to the stream host the target of the stream replies to the initiator with the
     * JID of the SOCKS5 host that they used.
     *
     * @param JID The JID of the used host.
     */
    public void setUsedHost(final String JID) {
        this.usedHost = new StreamHostUsed(JID);
    }

    /**
     * Returns the SOCKS5 host connected to by the remote user.
     *
     * @return Returns the SOCKS5 host connected to by the remote user.
     */
    public StreamHostUsed getUsedHost() {
        return usedHost;
    }

    /**
     * Returns the activate element of the packet sent to the proxy host to verify the identity of
     * the initiator and match them to the appropriate stream.
     *
     * @return Returns the activate element of the packet sent to the proxy host to verify the
     * identity of the initiator and match them to the appropriate stream.
     */
    public Activate getToActivate() {
        return toActivate;
    }

    /**
     * Upon the response from the target of the used host the activate packet is sent to the SOCKS5
     * proxy. The proxy will activate the stream or return an error after verifying the identity of
     * the initiator, using the activate packet.
     *
     * @param targetID The JID of the target of the file transfer.
     */
    public void setToActivate(final String targetID) {
        this.toActivate = new Activate(targetID);
    }

    public String getChildElementXML() {
        StringBuilder buf = new StringBuilder();

        buf.append("<query xmlns=\"http://jabber.org/protocol/bytestreams\"");
        if (this.getType().equals(IQ.Type.SET)) {
            if (getSessionID() != null) {
                buf.append(" sid=\"").append(getSessionID()).append("\"");
            }
            if (getMode() != null) {
                buf.append(" mode = \"").append(getMode()).append("\"");
            }
            buf.append(">");
            if (getToActivate() == null) {
                for (StreamHost streamHost : getStreamHosts()) {
                    buf.append(streamHost.toXML());
                }
            } else {
                buf.append(getToActivate().toXML());
            }
        } else if (this.getType().equals(IQ.Type.RESULT)) {
            buf.append(">");
            if (getUsedHost() != null) {
                buf.append(getUsedHost().toXML());
            }
            // A result from the server can also contain stream hosts
            else if (countStreamHosts() > 0) {
                for (StreamHost host : streamHosts) {
                    buf.append(host.toXML());
                }
            }
        } else if (this.getType().equals(IQ.Type.GET)) {
            return buf.append("/>").toString();
        } else {
            return null;
        }
        buf.append("</query>");

        return buf.toString();
    }

    /**
     * Packet extension that represents a potential SOCKS5 proxy for the file transfer. Stream hosts
     * are forwarded to the target of the file transfer who then chooses and connects to one.
     *
     * @author Alexander Wenckus
     */
    public static class StreamHost implements PacketExtension {

        public static String NAMESPACE = "";

        public static String ELEMENTNAME = "streamhost";

        private final String JID;

        private final String addy;

        private int port = 0;

        /**
         * Default constructor.
         *
         * @param JID     The JID of the stream host.
         * @param address The internet address of the stream host.
         */
        public StreamHost(final String JID, final String address) {
            this.JID = JID;
            this.addy = address;
        }

        /**
         * Returns the JID of the stream host.
         *
         * @return Returns the JID of the stream host.
         */
        public String getJID() {
            return JID;
        }

        /**
         * Returns the internet address of the stream host.
         *
         * @return Returns the internet address of the stream host.
         */
        public String getAddress() {
            return addy;
        }

        /**
         * Sets the port of the stream host.
         *
         * @param port The port on which the potential stream host would accept the connection.
         */
        public void setPort(final int port) {
            this.port = port;
        }

        /**
         * Returns the port on which the potential stream host would accept the connection.
         *
         * @return Returns the port on which the potential stream host would accept the connection.
         */
        public int getPort() {
            return port;
        }

        public String getNamespace() {
            return NAMESPACE;
        }

        public String getElementName() {
            return ELEMENTNAME;
        }

        public String toXML() {
            StringBuilder buf = new StringBuilder();

            buf.append("<").append(getElementName()).append(" ");
            buf.append("jid=\"").append(getJID()).append("\" ");
            buf.append("host=\"").append(getAddress()).append("\" ");
            if (getPort() != 0) {
                buf.append("port=\"").append(getPort()).append("\"");
            } else {
                buf.append("zeroconf=\"_jabber.bytestreams\"");
            }
            buf.append("/>");

            return buf.toString();
        }
    }

    /**
     * After selected a SOCKS5 stream host and successfully connecting, the target of the file
     * transfer returns a byte stream packet with the stream host used extension.
     *
     * @author Alexander Wenckus
     */
    public static class StreamHostUsed implements PacketExtension {

        public String NAMESPACE = "";

        public static String ELEMENTNAME = "streamhost-used";

        private final String JID;

        /**
         * Default constructor.
         *
         * @param JID The JID of the selected stream host.
         */
        public StreamHostUsed(final String JID) {
            this.JID = JID;
        }

        /**
         * Returns the JID of the selected stream host.
         *
         * @return Returns the JID of the selected stream host.
         */
        public String getJID() {
            return JID;
        }

        public String getNamespace() {
            return NAMESPACE;
        }

        public String getElementName() {
            return ELEMENTNAME;
        }

        public String toXML() {
            StringBuilder buf = new StringBuilder();
            buf.append("<").append(getElementName()).append(" ");
            buf.append("jid=\"").append(getJID()).append("\" ");
            buf.append("/>");
            return buf.toString();
        }
    }

    /**
     * The packet sent by the stream initiator to the stream proxy to activate the connection.
     *
     * @author Alexander Wenckus
     */
    public static class Activate implements PacketExtension {

        public String NAMESPACE = "";

        public static String ELEMENTNAME = "activate";

        private final String target;

        /**
         * Default constructor specifying the target of the stream.
         *
         * @param target The target of the stream.
         */
        public Activate(final String target) {
            this.target = target;
        }

        /**
         * Returns the target of the activation.
         *
         * @return Returns the target of the activation.
         */
        public String getTarget() {
            return target;
        }

        public String getNamespace() {
            return NAMESPACE;
        }

        public String getElementName() {
            return ELEMENTNAME;
        }

        public String toXML() {
            StringBuilder buf = new StringBuilder();
            buf.append("<").append(getElementName()).append(">");
            buf.append(getTarget());
            buf.append("</").append(getElementName()).append(">");
            return buf.toString();
        }
    }

    /**
     * The stream can be either a TCP stream or a UDP stream.
     *
     * @author Alexander Wenckus
     */
    public enum Mode {

        /**
         * A TCP based stream.
         */
        tcp,

        /**
         * A UDP based stream.
         */
        udp;

        public static Mode fromName(String name) {
            Mode mode;
            try {
                mode = Mode.valueOf(name);
            } catch (Exception ex) {
                mode = tcp;
            }

            return mode;
        }
    }
}
