/*
 * Copyright (c) 2006-2012 DMDirc Developers
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */
package com.dmdirc.parser.irc;

import com.dmdirc.parser.common.BaseParser;
import com.dmdirc.parser.common.ChannelJoinRequest;
import com.dmdirc.parser.common.ChildImplementations;
import com.dmdirc.parser.common.CompositionState;
import com.dmdirc.parser.common.IgnoreList;
import com.dmdirc.parser.common.MyInfo;
import com.dmdirc.parser.common.ParserError;
import com.dmdirc.parser.common.QueuePriority;
import com.dmdirc.parser.common.SystemEncoder;
import com.dmdirc.parser.interfaces.Encoder;
import com.dmdirc.parser.interfaces.EncodingParser;
import com.dmdirc.parser.interfaces.SecureParser;
import com.dmdirc.parser.interfaces.callbacks.*; //NOPMD
import com.dmdirc.parser.irc.IRCReader.ReadLine;
import com.dmdirc.parser.irc.outputqueue.OutputQueue;

import java.io.IOException;
import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Proxy;
import java.net.Socket;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.UnknownHostException;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.Deque;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.Timer;
import java.util.concurrent.Semaphore;
import java.util.concurrent.atomic.AtomicBoolean;

import javax.net.ssl.KeyManager;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;

/**
 * IRC Parser.
 */
@ChildImplementations({
    IRCChannelClientInfo.class,
    IRCChannelInfo.class,
    IRCClientInfo.class
})
public class IRCParser extends BaseParser implements SecureParser, EncodingParser {

    /** Max length an outgoing line should be (NOT including \r\n). */
    public static final int MAX_LINELENGTH = 510;
    /** General Debug Information. */
    public static final int DEBUG_INFO = 1;
    /** Socket Debug Information. */
    public static final int DEBUG_SOCKET = 2;
    /** Processing Manager Debug Information. */
    public static final int DEBUG_PROCESSOR = 4;
    /** List Mode Queue Debug Information. */
    public static final int DEBUG_LMQ = 8;

    /** Attempt to update user host all the time, not just on Who/Add/NickChange. */
    static final boolean ALWAYS_UPDATECLIENT = true;
    /** Byte used to show that a non-boolean mode is a list (b). */
    static final byte MODE_LIST = 1;
    /** Byte used to show that a non-boolean mode is not a list, and requires a parameter to set (lk). */
    static final byte MODE_SET = 2;
    /** Byte used to show that a non-boolean mode is not a list, and requires a parameter to unset (k). */
    static final byte MODE_UNSET = 4;
    /**
     * This is what the user wants settings to be.
     * Nickname here is *not* always accurate.<br><br>
     * ClientInfo variable tParser.getMyself() should be used for accurate info.
     */
    private MyInfo me = new MyInfo();
    /** Should PINGs be sent to the server to check if its alive? */
    private boolean checkServerPing = true;
    /** Timer for server ping. */
    private Timer pingTimer = null;
    /** Semaphore for access to pingTimer. */
    private final Semaphore pingTimerSem = new Semaphore(1);
    /** Is a ping needed? */
    private final AtomicBoolean pingNeeded = new AtomicBoolean(false);
    /** Time last ping was sent at. */
    private long pingTime;
    /** Current Server Lag. */
    private long serverLag;
    /** Last value sent as a ping argument. */
    private String lastPingValue = "";
    /**
     * Count down to next ping.
     * The timer fires every 10 seconds, this value is decreased every time the
     * timer fires.<br>
     * Once it reaches 0, we send a ping, and reset it to 6, this means we ping
     * the server every minute.
     *
     * @see setPingCountDownLength
     */
    private int pingCountDown;

    /** Network name. This is "" if no network name is provided */
    String networkName;
    /** This is what we think the nickname should be. */
    String thinkNickname;
    /** When using inbuilt pre-001 NickInUse handler, have we tried our AltNick. */
    boolean triedAlt;
    /** Have we received the 001. */
    boolean got001;
    /** Have we fired post005? */
    boolean post005;
    /** Has the thread started execution yet, (Prevents run() being called multiple times). */
    boolean hasBegan;
    /** Connect timeout. */
    private int connectTimeout = 5000;
    /** Hashtable storing known prefix modes (ohv). */
    final Map<Character, Long> prefixModes = new HashMap<Character, Long>();
    /**
     * Hashtable maping known prefix modes (ohv) to prefixes (@%+) - Both ways.
     * Prefix map contains 2 pairs for each mode. (eg @ => o and o => @)
     */
    final Map<Character, Character> prefixMap = new HashMap<Character, Character>();
    /** Integer representing the next avaliable integer value of a prefix mode. */
    long nextKeyPrefix = 1;
    /** Hashtable storing known user modes (owxis etc). */
    final Map<Character, Long> userModes = new HashMap<Character, Long>();
    /** Integer representing the next avaliable integer value of a User mode. */
    long nNextKeyUser = 1;
    /**
     * Hashtable storing known boolean chan modes (cntmi etc).
     * Valid Boolean Modes are stored as Hashtable.pub('m',1); where 'm' is the mode and 1 is a numeric value.<br><br>
     * Numeric values are powers of 2. This allows up to 32 modes at present (expandable to 64)<br><br>
     * ChannelInfo/ChannelClientInfo etc provide methods to view the modes in a human way.<br><br>
     * <br>
     * Channel modes discovered but not listed in 005 are stored as boolean modes automatically (and a ERROR_WARNING Error is called)
     */
    final Map<Character, Long> chanModesBool = new HashMap<Character, Long>();
    /** Integer representing the next avaliable integer value of a Boolean mode. */
    long nextKeyCMBool = 1;
    /**
     * Hashtable storing known non-boolean chan modes (klbeI etc).
     * Non Boolean Modes (for Channels) are stored together in this hashtable, the value param
     * is used to show the type of variable. (List (1), Param just for set (2), Param for Set and Unset (2+4=6))<br><br>
     * <br>
     * see MODE_LIST<br>
     * see MODE_SET<br>
     * see MODE_UNSET<br>
     */
    final Map<Character, Byte> chanModesOther = new HashMap<Character, Byte>();
    /** The last line of input received from the server */
    private ReadLine lastLine = null;
    /** Should the lastline (where given) be appended to the "data" part of any onErrorInfo call? */
    private boolean addLastLine = false;
    /** Channel Prefixes (ie # + etc). */
    private final List<Character> chanPrefix = Collections.synchronizedList(new LinkedList<Character>());
    /** Hashtable storing all known clients based on nickname (in lowercase). */
    private final Map<String, IRCClientInfo> clientList = new HashMap<String, IRCClientInfo>();
    /** Hashtable storing all known channels based on chanel name (inc prefix - in lowercase). */
    private final Map<String, IRCChannelInfo> channelList = new HashMap<String, IRCChannelInfo>();
    /** Reference to the ClientInfo object that references ourself. */
    private IRCClientInfo myself = new IRCClientInfo(this, "myself").setFake(true);
    /** Hashtable storing all information gathered from 005. */
    final Map<String, String> h005Info = new HashMap<String, String>();
    /** Does this server support timestamped IRC? */
    boolean timestampedIRC = false;
    /** difference in ms between our time and the servers time. */
    long tsdiff;
    /** Reference to the Processing Manager. */
    private final ProcessingManager myProcessingManager = new ProcessingManager(this);
    /** Should we automatically disconnect on fatal errors?. */
    private boolean disconnectOnFatal = true;
    /** Current Socket State. */
    protected SocketState currentSocketState = SocketState.NULL;
    /** This is the socket used for reading from/writing to the IRC server. */
    private Socket socket;
    /**
     * The underlying socket used for reading/writing to the IRC server.
     * For normal sockets this will be the same as {@link #socket} but for SSL
     * connections this will be the underlying {@link Socket} while
     * {@link #socket} will be an {@link SSLSocket}.
     */
    private Socket rawSocket;
    /** Used for writing to the server. */
    private OutputQueue out;
    /** The encoder to use to encode incoming lines. */
    private Encoder encoder = new SystemEncoder();
    /** Used for reading from the server. */
    private IRCReader in;
    /** This is the default TrustManager for SSL Sockets, it trusts all ssl certs. */
    private final TrustManager[] trustAllCerts = {
        new X509TrustManager() {

            @Override
            public X509Certificate[] getAcceptedIssuers() {
                return null;
            }

            @Override
            public void checkClientTrusted(final X509Certificate[] certs, final String authType) {
            }

            @Override
            public void checkServerTrusted(final X509Certificate[] certs, final String authType) {
            }
        },};
    /** Should channels automatically request list modes? */
    private boolean autoListMode = true;
    /** Should part/quit/kick callbacks be fired before removing the user internally? */
    private boolean removeAfterCallback = true;
    /** This is the TrustManager used for SSL Sockets. */
    private TrustManager[] myTrustManager = trustAllCerts;
    /** The KeyManagers used for client certificates for SSL sockets. */
    private KeyManager[] myKeyManagers;
    /** This is list containing 001 - 005 inclusive. */
    private final List<String> serverInformationLines = new LinkedList<String>();

    /**
     * Default constructor, ServerInfo and MyInfo need to be added separately (using IRC.me and IRC.server).
     */
    public IRCParser() {
        this((MyInfo) null);
    }

    /**
     * Constructor with ServerInfo, MyInfo needs to be added separately (using IRC.me).
     *
     * @param uri The URI to connect to
     */
    public IRCParser(final URI uri) {
        this(null, uri);
    }

    /**
     * Constructor with MyInfo, ServerInfo needs to be added separately (using IRC.server).
     *
     * @param myDetails Client information.
     */
    public IRCParser(final MyInfo myDetails) {
        this(myDetails, null);
    }

    /**
     * Creates a new IRCParser with the specified client details which will
     * connect to the specified URI.
     *
     * @since 0.6.3
     * @param myDetails The client details to use
     * @param uri The URI to connect to
     */
    public IRCParser(final MyInfo myDetails, final URI uri) {
        super(fixURI(uri));

        out = new OutputQueue();
        if (myDetails != null) {
            this.me = myDetails;
        }

        setIgnoreList(new IgnoreList());
        setPingTimerInterval(10000);
        setPingTimerFraction(6);
        resetState();
    }

    /**
     * Get the current OutputQueue
     *
     * @return the current OutputQueue
     */
    public OutputQueue getOutputQueue() {
        return out;
    }

    /** {@inheritDoc} */
    @Override
    public boolean compareURI(final URI uri) {
        // Get the old URI.
        final URI oldURI = getURI();
        final URI newURI = fixURI(uri);

        // Check that protocol, host and port are the same.
        // Anything else won't change the server we connect to just what we
        // would do after connecting, so is not relevent.
        return newURI.getScheme().equalsIgnoreCase(oldURI.getScheme())
                && newURI.getHost().equalsIgnoreCase(oldURI.getHost())
                && ((newURI.getUserInfo() == null || newURI.getUserInfo().isEmpty()) || newURI.getUserInfo().equalsIgnoreCase((oldURI.getUserInfo() == null ? "" : oldURI.getUserInfo())))
                && newURI.getPort() == oldURI.getPort();
    }

    /**
     * Check that the given URI makes sense.
     *
     * @param uri Suggested URI.
     * @return A "sensible" version of the given URI. (eg, with a default port)
     */
    private static URI fixURI(final URI checkURI) {
        if (checkURI == null) { return null; }
        // Starter URI
        URI uri = checkURI;

        // Make changes if required.

        // Default port.
        if (uri.getPort() == -1) {
            try {
                uri = new URI(uri.getScheme(), uri.getUserInfo(), uri.getHost(), 6667, uri.getPath(), uri.getQuery(), uri.getFragment());
            } catch (URISyntaxException ex) { /* Won't happen. */ }
        }

        // Other changes here...

        // Return the sensible URI.
        return uri;
    }

    /** {@inheritDoc} */
    @Override
    public Collection<? extends ChannelJoinRequest> extractChannels(final URI uri) {
        if (uri == null) {
            return Collections.<ChannelJoinRequest>emptyList();
        }

        String channelString = uri.getPath();
        if (uri.getRawQuery() != null && !uri.getRawQuery().isEmpty()) {
            channelString += "?" + uri.getRawQuery();
        }

        if (uri.getRawFragment() != null && !uri.getRawFragment().isEmpty()) {
            channelString += "#" + uri.getRawFragment();
        }

        if (!channelString.isEmpty() && channelString.charAt(0) == '/') {
            channelString = channelString.substring(1);
        }

        return extractChannels(channelString);
    }

    /**
     * Extracts a set of channels and optional keys from the specified String.
     * Channels are separated by commas, and keys are separated from their
     * channels by a space.
     *
     * @since 0.6.4
     * @param channels The string of channels to parse
     * @return A corresponding collection of join request objects
     */
    protected Collection<? extends ChannelJoinRequest> extractChannels(final String channels) {
        final List<ChannelJoinRequest> res = new ArrayList<ChannelJoinRequest>();

        for (String channel : channels.split(",")) {
            final String[] parts = channel.split(" ", 2);

            if (parts.length == 2) {
                res.add(new ChannelJoinRequest(parts[0], parts[1]));
            } else {
                res.add(new ChannelJoinRequest(parts[0]));
            }
        }

        return res;
    }

    /**
     * Get the current Value of autoListMode.
     *
     * @return Value of autoListMode (true if channels automatically ask for list modes on join, else false)
     */
    public boolean getAutoListMode() {
        return autoListMode;
    }

    /**
     * Set the current Value of autoListMode.
     *
     * @param newValue New value to set autoListMode
     */
    public void setAutoListMode(final boolean newValue) {
        autoListMode = newValue;
    }

    /**
     * Get the current Value of removeAfterCallback.
     *
     * @return Value of removeAfterCallback (true if kick/part/quit callbacks are fired before internal removal)
     */
    public boolean getRemoveAfterCallback() {
        return removeAfterCallback;
    }

    /**
     * Get the current Value of removeAfterCallback.
     *
     * @param newValue New value to set removeAfterCallback
     */
    public void setRemoveAfterCallback(final boolean newValue) {
        removeAfterCallback = newValue;
    }

    /**
     * Get the current Value of addLastLine.
     *
     * @return Value of addLastLine (true if lastLine info will be automatically
     *         added to the errorInfo data line). This should be true if lastLine
     *         isn't handled any other way.
     */
    public boolean getAddLastLine() {
        return addLastLine;
    }

    /**
     * Get the current Value of addLastLine.
     *
     * @param newValue New value to set addLastLine
     */
    public void setAddLastLine(final boolean newValue) {
        addLastLine = newValue;
    }

    /**
     * Get the current Value of connectTimeout.
     *
     * @return The value of getConnectTimeout.
     */
    public int getConnectTimeout() {
        return connectTimeout;
    }

    /**
     * Set the Value of connectTimeout.
     *
     * @param newValue new value for value of getConnectTimeout.
     */
    public void setConnectTimeout(final int newValue) {
        connectTimeout = newValue;
    }

    /**
     * Get the current socket State.
     *
     * @since 0.6.3m1
     * @return Current {@link SocketState}
     */
    public SocketState getSocketState() {
        return currentSocketState;
    }

    /**
     * Get a reference to the Processing Manager.
     *
     * @return Reference to the CallbackManager
     */
    public ProcessingManager getProcessingManager() {
        return myProcessingManager;
    }

    /**
     * Get a reference to the default TrustManager for SSL Sockets.
     *
     * @return a reference to trustAllCerts
     */
    public TrustManager[] getDefaultTrustManager() {
        return trustAllCerts;
    }

    /**
     * Get a reference to the current TrustManager for SSL Sockets.
     *
     * @return a reference to myTrustManager;
     */
    public TrustManager[] getTrustManager() {
        return myTrustManager;
    }

    /** {@inheritDoc} */
    @Override
    public void setTrustManagers(final TrustManager[] managers) {
        myTrustManager = managers;
    }

    /** {@inheritDoc} */
    @Override
    public void setKeyManagers(final KeyManager[] managers) {
        myKeyManagers = managers;
    }

    //---------------------------------------------------------------------------
    // Start Callbacks
    //---------------------------------------------------------------------------

    /**
     * Callback to all objects implementing the ServerError Callback.
     *
     * @see com.dmdirc.parser.irc.callbacks.interfaces.IServerError
     * @param message The error message
     */
    protected void callServerError(final String message) {
        getCallback(ServerErrorListener.class).onServerError(null, null, message);
    }

    /**
     * Callback to all objects implementing the DataIn Callback.
     *
     * @see com.dmdirc.parser.irc.callbacks.interfaces.IDataIn
     * @param data Incoming Line.
     */
    protected void callDataIn(final String data) {
        getCallback(DataInListener.class).onDataIn(null, null, data);
    }

    /**
     * Callback to all objects implementing the DataOut Callback.
     *
     * @param data Outgoing Data
     * @param fromParser True if parser sent the data, false if sent using .sendLine
     * @see com.dmdirc.parser.irc.callbacks.interfaces.IDataOut
     */
    protected void callDataOut(final String data, final boolean fromParser) {
        getCallback(DataOutListener.class).onDataOut(null, null, data, fromParser);
    }

    /**
     * Callback to all objects implementing the DebugInfo Callback.
     *
     * @see com.dmdirc.parser.irc.callbacks.interfaces.IDebugInfo
     * @param level Debugging Level (DEBUG_INFO, DEBUG_SOCKET etc)
     * @param data Debugging Information as a format string
     * @param args Formatting String Options
     */
    protected void callDebugInfo(final int level, final String data, final Object... args) {
        callDebugInfo(level, String.format(data, args));
    }

    /**
     * Callback to all objects implementing the DebugInfo Callback.
     *
     * @see com.dmdirc.parser.irc.callbacks.interfaces.IDebugInfo
     * @param level Debugging Level (DEBUG_INFO, DEBUG_SOCKET etc)
     * @param data Debugging Information
     */
    protected void callDebugInfo(final int level, final String data) {
        getCallback(DebugInfoListener.class).onDebugInfo(null, null, level, data);
    }

    /**
     * Callback to all objects implementing the IErrorInfo Interface.
     *
     * @see com.dmdirc.parser.irc.callbacks.interfaces.IErrorInfo
     * @param errorInfo ParserError object representing the error.
     */
    protected void callErrorInfo(final ParserError errorInfo) {
        getCallback(ErrorInfoListener.class).onErrorInfo(null, null, errorInfo);
    }

    /**
     * Callback to all objects implementing the IConnectError Interface.
     *
     * @see com.dmdirc.parser.irc.callbacks.interfaces.IConnectError
     * @param errorInfo ParserError object representing the error.
     */
    protected void callConnectError(final ParserError errorInfo) {
        getCallback(ConnectErrorListener.class).onConnectError(null, null, errorInfo);
    }

    /**
     * Callback to all objects implementing the SocketClosed Callback.
     *
     * @see com.dmdirc.parser.irc.callbacks.interfaces.ISocketClosed
     */
    protected void callSocketClosed() {
        getCallback(SocketCloseListener.class).onSocketClosed(null, null);
    }

    /**
     * Callback to all objects implementing the PingFailed Callback.
     *
     * @see com.dmdirc.parser.irc.callbacks.interfaces.IPingFailed
     * @return True if any callback was called, false otherwise.
     */
    protected boolean callPingFailed() {
        return getCallbackManager().getCallbackType(PingFailureListener.class).call();
    }

    /**
     * Callback to all objects implementing the PingSent Callback.
     *
     * @see com.dmdirc.parser.irc.callbacks.interfaces.IPingSent
     */
    protected void callPingSent() {
        getCallback(PingSentListener.class).onPingSent(null, null);
    }

    /**
     * Callback to all objects implementing the PingSuccess Callback.
     *
     * @see com.dmdirc.parser.irc.callbacks.interfaces.IPingSuccess
     */
    protected void callPingSuccess() {
        getCallback(PingSuccessListener.class).onPingSuccess(null, null);
    }

    /**
     * Callback to all objects implementing the Post005 Callback.
     *
     * @see IPost005
     */
    protected synchronized void callPost005() {
        if (post005) {
            return;
        }

        post005 = true;

        if (!h005Info.containsKey("CHANTYPES")) {
            parseChanPrefix();
        }
        if (!h005Info.containsKey("PREFIX")) {
            parsePrefixModes();
        }
        if (!h005Info.containsKey("USERMODES")) {
            parseUserModes();
        }
        if (!h005Info.containsKey("CHANMODES")) {
            parseChanModes();
        }

        getCallback(ServerReadyListener.class).onServerReady(null, null);
    }

    //---------------------------------------------------------------------------
    // End Callbacks
    //---------------------------------------------------------------------------
    /** Reset internal state (use before doConnect). */
    private void resetState() {
        // Reset General State info
        triedAlt = false;
        got001 = false;
        post005 = false;
        // Clear the hash tables
        channelList.clear();
        clientList.clear();
        h005Info.clear();
        prefixModes.clear();
        prefixMap.clear();
        chanModesOther.clear();
        chanModesBool.clear();
        userModes.clear();
        chanPrefix.clear();
        // Clear output queue.
        if (out != null) {
            out.clearQueue();
        }
        // Reset the mode indexes
        nextKeyPrefix = 1;
        nextKeyCMBool = 1;
        nNextKeyUser = 1;
        setServerName("");
        networkName = "";
        lastLine = null;
        myself = new IRCClientInfo(this, "myself").setFake(true);

        synchronized (serverInformationLines) {
            serverInformationLines.clear();
        }
        stopPingTimer();

        currentSocketState = SocketState.CLOSED;
        setEncoding(IRCEncoding.RFC1459);
    }

    /**
     * Called after other error callbacks.
     * CallbackOnErrorInfo automatically calls this *AFTER* any registered callbacks
     * for it are called.
     *
     * @param errorInfo ParserError object representing the error.
     * @param called True/False depending on the the success of other callbacks.
     */
    public void onPostErrorInfo(final ParserError errorInfo, final boolean called) {
        if (errorInfo.isFatal() && disconnectOnFatal) {
            disconnect("Fatal Parser Error");
        }
    }

    /**
     * Get the current Value of disconnectOnFatal.
     *
     * @return Value of disconnectOnFatal (true if the parser automatically disconnects on fatal errors, else false)
     */
    public boolean getDisconnectOnFatal() {
        return disconnectOnFatal;
    }

    /**
     * Set the current Value of disconnectOnFatal.
     *
     * @param newValue New value to set disconnectOnFatal
     */
    public void setDisconnectOnFatal(final boolean newValue) {
        disconnectOnFatal = newValue;
    }

    private Socket newSocket(final URI target, final URI proxy) throws IOException {
        if (target.getPort() > 65535 || target.getPort() <= 0) {
            throw new IOException("Server port (" + target.getPort() + ") is invalid.");
        }

        Socket mySocket = null;
        if (proxy == null) {
            callDebugInfo(DEBUG_SOCKET, "Not using Proxy");
            mySocket = new Socket();

            if (getBindIP() != null && !getBindIP().isEmpty()) {
                callDebugInfo(DEBUG_SOCKET, "Binding to IP: " + getBindIP());
                try {
                    mySocket.bind(new InetSocketAddress(InetAddress.getByName(getBindIP()), 0));
                } catch (IOException e) {
                    callDebugInfo(DEBUG_SOCKET, "Binding failed: " + e.getMessage());
                }
            }

            mySocket.connect(new InetSocketAddress(target.getHost(), target.getPort()), connectTimeout);
        } else {
            callDebugInfo(DEBUG_SOCKET, "Using Proxy");

            if (getBindIP() != null && !getBindIP().isEmpty()) {
                callDebugInfo(DEBUG_SOCKET, "IP Binding is not possible when using a proxy.");
            }

            final String proxyHost = proxy.getHost();
            final int proxyPort = proxy.getPort();

            if (proxyPort > 65535 || proxyPort <= 0) {
                throw new IOException("Proxy port (" + proxyPort + ") is invalid.");
            }

            final Proxy.Type proxyType = Proxy.Type.valueOf(proxy.getScheme().toUpperCase());
            mySocket = new Socket(new Proxy(proxyType, new InetSocketAddress(proxyHost, proxyPort)));

            final IRCAuthenticator ia = IRCAuthenticator.getIRCAuthenticator();

            try {
                try {
                    ia.getSemaphore().acquire();
                } catch (InterruptedException ex) {
                }

                ia.addAuthentication(target, proxy);
                mySocket.connect(new InetSocketAddress(target.getHost(), target.getPort()), connectTimeout);
            } finally {
                ia.removeAuthentication(target, proxy);
                ia.getSemaphore().release();
            }
        }

        return mySocket;
    }

    /**
     * Find a socket to connect using, this will where possible attempt IPv6
     * first, before falling back to IPv4.
     *
     * This will:
     *   - Try using v6 first before v4.
     *   - If there are any failures at all with v6 (including, not having a
     *     v6 address...) then fall through to v4.
     *   - If there is no v4 address, throw the v6 exception
     *   - Otherwise, try v4
     *   - If there are failures with v4, throw the exception.
     *
     * If we have both IPv6 and IPv4, and a target host has both IPv6 and IPv4
     * addresses, but does not listen on the requested port on either, this
     * will cause a double-length connection timeout.
     *
     * @param target Target URI to connect to.
     * @param proxy Proxy URI to use
     * @return Socket to use in future connections.
     * @throws IOException if there is an error.
     */
    private Socket findSocket(final URI target, final URI proxy) throws IOException {
        URI target6 = null;
        URI target4 = null;

        // Get the v6 and v4 addresses if appropriate..
        try {
            for (InetAddress i : InetAddress.getAllByName(target.getHost())) {
                if (target6 == null && i instanceof Inet6Address) {
                    target6 = new URI(target.getScheme(), target.getUserInfo(), i.getHostAddress(), target.getPort(), target.getPath(), target.getQuery(), target.getFragment());
                } else if (target4 == null && i instanceof Inet4Address) {
                    target4 = new URI(target.getScheme(), target.getUserInfo(), i.getHostAddress(), target.getPort(), target.getPath(), target.getQuery(), target.getFragment());
                }
            }
        } catch (final URISyntaxException use) { /* Won't happen. */ }

        // Now try and connect.
        // Try using v6 first before v4.
        // If there are any failures at all with v6 (including, not having a
        // v6 address...) then fall through to v4.
        // If there is no v4 address, throw the v6 exception
        // Otherwise, try v4
        // If there are failures with v4, throw the exception.
        //
        // If we have both IPv6 and IPv4, and a target host has both IPv6 and
        // IPv4 addresses, but does not listen on the requested port on either,
        // this will cause a double-length connection timeout.
        //
        // In future this may want to be rewritten to perform a happy-eyeballs
        // race rather than trying one then the other.
        Exception v6Exception = null;
        if (target6 != null) {
            try {
                return newSocket(target6, proxy);
            } catch (final IOException ioe) {
                if (target4 == null) {
                    throw ioe;
                }
            } catch (final Exception e) {
                v6Exception = e;
                callDebugInfo(DEBUG_SOCKET, "Exception trying to use IPv6: " + e);
            }
        }
        if (target4 != null) {
            try {
                return newSocket(target4, proxy);
            } catch (final IOException e2) {
                callDebugInfo(DEBUG_SOCKET, "Exception trying to use IPv4: " + e2);
                throw e2;
            }
        } else if (v6Exception != null) {
            throw new IOException("Error connecting to: " + target + " (IPv6 failure, with no IPv4 fallback)", v6Exception);
        } else {
            // We should never get here as if both target4 and target6 were null
            // getAllByName would have thrown an exception instead.
            throw new IOException("Error connecting to: " + target + " (General connectivity failure)");
        }
    }

    /**
     * Connect to IRC.
     *
     * @throws IOException if the socket can not be connected
     * @throws NoSuchAlgorithmException if SSL is not available
     * @throws KeyManagementException if the trustManager is invalid
     */
    private void doConnect() throws IOException, NoSuchAlgorithmException, KeyManagementException {
        if (getURI() == null || getURI().getHost() == null) {
            throw new UnknownHostException("Unspecified host.");
        }

        resetState();
        callDebugInfo(DEBUG_SOCKET, "Connecting to " + getURI().getHost() + ":" + getURI().getPort());

        currentSocketState = SocketState.OPENING;
        socket = findSocket(getURI(), getProxy());

        rawSocket = socket;

        if (getURI().getScheme().endsWith("s")) {
            callDebugInfo(DEBUG_SOCKET, "Server is SSL.");

            if (myTrustManager == null) {
                myTrustManager = trustAllCerts;
            }

            final SSLContext sc = SSLContext.getInstance("SSL");
            sc.init(myKeyManagers, myTrustManager, new java.security.SecureRandom());

            final SSLSocketFactory socketFactory = sc.getSocketFactory();
            socket = socketFactory.createSocket(socket, getURI().getHost(), getURI().getPort(), false);

            // Manually start a handshake so we get proper SSL errors here,
            // and so that we can control the connection timeout
            final int timeout = socket.getSoTimeout();
            socket.setSoTimeout(10000);
            ((SSLSocket) socket).startHandshake();
            socket.setSoTimeout(timeout);

            currentSocketState = SocketState.OPENING;
        }

        callDebugInfo(DEBUG_SOCKET, "\t-> Opening socket output stream PrintWriter");
        out.setOutputStream(socket.getOutputStream());
        out.setQueueEnabled(true);
        currentSocketState = SocketState.OPEN;
        callDebugInfo(DEBUG_SOCKET, "\t-> Opening socket input stream BufferedReader");
        in = new IRCReader(socket.getInputStream(), encoder);
        callDebugInfo(DEBUG_SOCKET, "\t-> Socket Opened");
    }

    /**
     * Send server connection strings (NICK/USER/PASS).
     */
    protected void sendConnectionStrings() {
        if (getURI().getUserInfo() != null && !getURI().getUserInfo().isEmpty()) {
            sendString("PASS " + getURI().getUserInfo());
        }
        sendString("NICK " + me.getNickname());
        thinkNickname = me.getNickname();
        String localhost;
        try {
            localhost = InetAddress.getLocalHost().getHostAddress();
        } catch (UnknownHostException uhe) {
            localhost = "*";
        }
        sendString("USER " + me.getUsername() + " " + localhost + " " + getURI().getHost() + " :" + me.getRealname());
    }

    /**
     * Handle an onConnect error.
     *
     * @param e Exception to handle
     * @param isUserError Is this a user error?
     */
    private void handleConnectException(final Exception e, final boolean isUserError) {
        callDebugInfo(DEBUG_SOCKET, "Error Connecting (" + e.getMessage() + "), Aborted");
        final ParserError ei = new ParserError(ParserError.ERROR_ERROR + (isUserError ? ParserError.ERROR_USER : 0), "Exception with server socket", getLastLine());
        ei.setException(e);
        callConnectError(ei);

        if (currentSocketState != SocketState.CLOSED) {
            currentSocketState = SocketState.CLOSED;
            callSocketClosed();
        }
        resetState();
    }

    /**
     * Begin execution.
     * Connect to server, and start parsing incoming lines
     */
    @Override
    public void run() {
        callDebugInfo(DEBUG_INFO, "Begin Thread Execution");
        if (hasBegan) {
            return;
        } else {
            hasBegan = true;
        }
        try {
            doConnect();
        } catch (UnknownHostException e) {
            handleConnectException(e, true);
            return;
        } catch (IOException e) {
            handleConnectException(e, true);
            return;
        } catch (NoSuchAlgorithmException e) {
            handleConnectException(e, false);
            return;
        } catch (KeyManagementException e) {
            handleConnectException(e, false);
            return;
        }

        callDebugInfo(DEBUG_SOCKET, "Socket Connected");

        sendConnectionStrings();

        while (true) {
            try {
                lastLine = in.readLine(); // Blocking :/
                if (lastLine == null) {
                    if (currentSocketState != SocketState.CLOSED) {
                        currentSocketState = SocketState.CLOSED;
                        callSocketClosed();
                    }
                    resetState();
                    break;
                } else if (currentSocketState != SocketState.CLOSING) {
                    processLine(lastLine);
                }
            } catch (IOException e) {
                callDebugInfo(DEBUG_SOCKET, "Exception in main loop (" + e.getMessage() + "), Aborted");

                if (currentSocketState != SocketState.CLOSED) {
                    currentSocketState = SocketState.CLOSED;
                    callSocketClosed();
                }
                resetState();
                break;
            }
        }
        callDebugInfo(DEBUG_INFO, "End Thread Execution");
    }

    /** {@inheritDoc} */
    @Override
    public int getLocalPort() {
        if (currentSocketState == SocketState.OPENING || currentSocketState == SocketState.OPEN) {
            return socket.getLocalPort();
        } else {
            return 0;
        }
    }

    /** Close socket on destroy. */
    @Override
    protected void finalize() throws Throwable {
        try {
            // See note at disconnect() method for why we close rawSocket.
            if (rawSocket != null) {
                rawSocket.close();
            }
        } catch (IOException e) {
            callDebugInfo(DEBUG_SOCKET, "Could not close socket");
        }
        super.finalize();
    }

    /**
     * Get the trailing parameter for a line.
     * The parameter is everything after the first occurance of " :" ot the last token in the line after a space.
     *
     * @param line Line to get parameter for
     * @return Parameter of the line
     */
    public static String getParam(final String line) {
        String[] params = null;
        params = line.split(" :", 2);
        return params[params.length - 1];
    }

    /**
     * Tokenise a line.
     * splits by " " up to the first " :" everything after this is a single token
     *
     * @param line Line to tokenise
     * @return Array of tokens
     */
    public static String[] tokeniseLine(final String line) {
        if (line == null) {
            return new String[]{""}; // Return empty string[]
        }

        final int lastarg = line.indexOf(" :");
        String[] tokens;

        if (lastarg > -1) {
            final String[] temp = line.substring(0, lastarg).split(" ");
            tokens = new String[temp.length + 1];
            System.arraycopy(temp, 0, tokens, 0, temp.length);
            tokens[temp.length] = line.substring(lastarg + 2);
        } else {
            tokens = line.split(" ");
        }

        if (tokens.length < 1) {
            tokens = new String[]{""};
        }

        return tokens;
    }

    /** {@inheritDoc} */
    @Override
    public IRCClientInfo getClient(final String details) {
        final String sWho = getStringConverter().toLowerCase(IRCClientInfo.parseHost(details));

        if (clientList.containsKey(sWho)) {
            return clientList.get(sWho);
        } else {
            return new IRCClientInfo(this, details).setFake(true);
        }
    }

    public boolean isKnownClient(final String host) {
        final String sWho = getStringConverter().toLowerCase(IRCClientInfo.parseHost(host));
        return clientList.containsKey(sWho);
    }

    /** {@inheritDoc} */
    @Override
    public IRCChannelInfo getChannel(final String channel) {
        synchronized (channelList) {
            return channelList.get(getStringConverter().toLowerCase(channel));
        }
    }

    /** {@inheritDoc} */
    @Override
    public void sendInvite(final String channel, final String user) {
        sendRawMessage("INVITE " + user + " " + channel);
    }

    /** {@inheritDoc} */
    @Override
    public void sendRawMessage(final String message) {
        doSendString(message, QueuePriority.NORMAL, false);
    }

    /** {@inheritDoc} */
    @Override
    public void sendRawMessage(final String message, final QueuePriority priority) {
        doSendString(message, priority, false);
    }

    /**
     * Send a line to the server and add proper line ending.
     *
     * @param line Line to send (\r\n termination is added automatically)
     * @return True if line was sent, else false.
     */
    protected boolean sendString(final String line) {
        return doSendString(line, QueuePriority.NORMAL, true);
    }

    /**
     * Send a line to the server and add proper line ending.
     * If a non-empty argument is given, it is appended as a trailing argument
     * (i.e., separated by " :"); otherwise, the line is sent as-is.
     *
     * @param line Line to send
     * @param argument Trailing argument for the command, if any
     * @return True if line was sent, else false.
     */
    protected boolean sendString(final String line, final String argument) {
        return sendString(argument.isEmpty() ? line : line + " :" + argument);
    }

    /**
     * Send a line to the server and add proper line ending.
     *
     * @param line Line to send (\r\n termination is added automatically)
     * @param priority Priority of this line.
     * @return True if line was sent, else false.
     */
    protected boolean sendString(final String line, final QueuePriority priority) {
        return doSendString(line, priority, true);
    }

    /**
     * Send a line to the server and add proper line ending.
     *
     * @param line Line to send (\r\n termination is added automatically)
     * @param priority Priority of this line.
     * @param fromParser is this line from the parser? (used for callDataOut)
     * @return True if line was sent, else false.
     */
    protected boolean doSendString(final String line, final QueuePriority priority, final boolean fromParser) {
        if (out == null || getSocketState() != SocketState.OPEN) {
            return false;
        }
        callDataOut(line, fromParser);
        out.sendLine(line, priority);
        final String[] newLine = tokeniseLine(line);
        if (newLine[0].equalsIgnoreCase("away") && newLine.length > 1) {
            myself.setAwayReason(newLine[newLine.length - 1]);
        } else if (newLine[0].equalsIgnoreCase("mode") && newLine.length == 3) {
            final IRCChannelInfo channel = getChannel(newLine[1]);
            if (channel != null) {
                // This makes sure we don't add the same item to the LMQ twice,
                // even if its requested twice, as the ircd will only reply once
                final Deque<Character> foundModes = new LinkedList<Character>();
                final Queue<Character> listModeQueue = channel.getListModeQueue();
                for (int i = 0; i < newLine[2].length(); ++i) {
                    final Character mode = newLine[2].charAt(i);
                    callDebugInfo(DEBUG_LMQ, "Intercepted mode request for " + channel + " for mode " + mode);
                    if (chanModesOther.containsKey(mode) && chanModesOther.get(mode) == MODE_LIST) {
                        if (foundModes.contains(mode)) {
                            callDebugInfo(DEBUG_LMQ, "Already added to LMQ");
                        } else {
                            listModeQueue.offer(mode);
                            foundModes.offer(mode);
                            callDebugInfo(DEBUG_LMQ, "Added to LMQ");
                        }
                    }
                }
            }
        }

        return true;
    }

    /** {@inheritDoc} */
    @Override
    public String getNetworkName() {
        return networkName;
    }

    /** {@inheritDoc} */
    @Override
    public String getLastLine() {
        return lastLine == null ? "" : lastLine.getLine();
    }

    /** {@inheritDoc} */
    @Override
    public List<String> getServerInformationLines() {
        synchronized (serverInformationLines) {
            return new LinkedList<String>(serverInformationLines);
        }
    }

    /**
     * Process a line and call relevant methods for handling.
     *
     * @param line Line read from the IRC server
     */
    @SuppressWarnings("fallthrough")
    protected void processLine(final ReadLine line) {
        callDataIn(line.getLine());
        String[] token = line.getTokens();
        Date lineTS = new Date();
        if (!token[0].isEmpty() && timestampedIRC && token[0].charAt(0) == '@') {
            try {
                final int tsEnd = token[0].indexOf('@', 1);
                final long ts = Long.parseLong(token[0].substring(1, tsEnd));
                token[0] = token[0].substring(tsEnd + 1);
                lineTS = new Date(ts - tsdiff);
            } catch (final NumberFormatException nfe) { /* Do nothing. */ }
        }

        int nParam;
        setPingNeeded(false);

        if (token.length < 2) {
            return;
        }

        try {
            final String sParam = token[1];
            if (token[0].equalsIgnoreCase("PING") || token[1].equalsIgnoreCase("PING")) {
                sendString("PONG :" + sParam, QueuePriority.HIGH);
            } else if (token[0].equalsIgnoreCase("PONG") || token[1].equalsIgnoreCase("PONG")) {
                if (!lastPingValue.isEmpty() && lastPingValue.equals(token[token.length - 1])) {
                    lastPingValue = "";
                    serverLag = System.currentTimeMillis() - pingTime;
                    callPingSuccess();
                }
            } else if (token[0].equalsIgnoreCase("ERROR")) {
                final StringBuilder errorMessage = new StringBuilder();
                for (int i = 1; i < token.length; ++i) {
                    errorMessage.append(token[i]);
                }
                callServerError(errorMessage.toString());
            } else if (timestampedIRC && token[1].equalsIgnoreCase("TSIRC") && token.length > 3) {
                if (token[2].equals("1")) {
                    try {
                        final long ts = Long.parseLong(token[3]);
                        tsdiff = ts - System.currentTimeMillis();
                    } catch (final NumberFormatException nfe) { /* Do nothing. */ }
                } else {
                    timestampedIRC = false;
                }
            } else {
                if (got001) {
                    // Freenode sends a random notice in a stupid place, others might do aswell
                    // These shouldn't cause post005 to be fired, so handle them here.
                    if (token[0].equalsIgnoreCase("NOTICE") || (token.length > 2 && token[2].equalsIgnoreCase("NOTICE"))) {
                        try {
                            myProcessingManager.process(lineTS, "Notice Auth", token);
                        } catch (ProcessorNotFoundException e) {
                            // ???
                        }

                        return;
                    }

                    if (!post005) {
                        try {
                            nParam = Integer.parseInt(token[1]);
                        } catch (NumberFormatException e) {
                            nParam = -1;
                        }

                        if (nParam < 0 || nParam > 5) {
                            callPost005();
                        } else {
                            // Store 001 - 005 for informational purposes.
                            synchronized (serverInformationLines) {
                                serverInformationLines.add(line.getLine());
                            }
                        }
                    }
                    // After 001 we potentially care about everything!
                    try {
                        myProcessingManager.process(lineTS, sParam, token);
                    } catch (ProcessorNotFoundException e) {
                        // ???
                    }
                } else {
                    // Before 001 we don't care about much.
                    try {
                        nParam = Integer.parseInt(token[1]);
                    } catch (NumberFormatException e) {
                        nParam = -1;
                    }
                    switch (nParam) {
                        case 1: // 001 - Welcome to IRC
                            synchronized (serverInformationLines) {
                                serverInformationLines.add(line.getLine());
                            }
                            // Fallthrough
                        case 464: // Password Required
                        case 433: // Nick In Use
                            try {
                                myProcessingManager.process(sParam, token);
                            } catch (ProcessorNotFoundException e) {
                            }
                            break;
                        default: // Unknown - Send to Notice Auth
                            // Some networks send a CTCP during the auth process, handle it
                            if (token.length > 3 && !token[3].isEmpty() && token[3].charAt(0) == (char) 1 && token[3].charAt(token[3].length() - 1) == (char) 1) {
                                try {
                                    myProcessingManager.process(lineTS, sParam, token);
                                } catch (ProcessorNotFoundException e) {
                                }
                                break;
                            }
                            // Some networks may send a NICK message if you nick change before 001
                            // Eat it up so that it isn't treated as a notice auth.
                            if (token[1].equalsIgnoreCase("NICK")) {
                                break;
                            }

                            // Otherwise, send to Notice Auth
                            try {
                                myProcessingManager.process(lineTS, "Notice Auth", token);
                            } catch (ProcessorNotFoundException e) {
                            }
                            break;
                    }
                }
            }
        } catch (Exception e) {
            final ParserError ei = new ParserError(ParserError.ERROR_FATAL, "Fatal Exception in Parser.", getLastLine());
            ei.setException(e);
            callErrorInfo(ei);
        }
    }

    /** The IRCStringConverter for this parser */
    private IRCStringConverter stringConverter = null;

    /** {@inheritDoc} */
    @Override
    public IRCStringConverter getStringConverter() {
        if (stringConverter == null) {
            stringConverter = new IRCStringConverter(IRCEncoding.RFC1459);
        }
        return stringConverter;
    }

    /**
     * Sets the encoding that this parser's string converter should use.
     *
     * @param encoding The encoding to use
     */
    protected void setEncoding(final IRCEncoding encoding) {
        stringConverter = new IRCStringConverter(encoding);
    }

    /**
     * Process CHANMODES from 005.
     */
    public void parseChanModes() {
        final StringBuilder sDefaultModes = new StringBuilder("b,k,l,");
        String[] bits = null;
        String modeStr;
        if (h005Info.containsKey("USERCHANMODES")) {
            if (getServerType() == ServerType.DANCER) {
                sDefaultModes.insert(0, "dqeI");
            } else if (getServerType() == ServerType.AUSTIRC) {
                sDefaultModes.insert(0, "e");
            }
            modeStr = h005Info.get("USERCHANMODES");
            char mode;
            for (int i = 0; i < modeStr.length(); ++i) {
                mode = modeStr.charAt(i);
                if (!prefixModes.containsKey(mode) && sDefaultModes.indexOf(Character.toString(mode)) < 0) {
                    sDefaultModes.append(mode);
                }
            }
        } else {
            sDefaultModes.append("imnpstrc");
        }
        if (h005Info.containsKey("CHANMODES")) {
            modeStr = h005Info.get("CHANMODES");
        } else {
            modeStr = sDefaultModes.toString();
            h005Info.put("CHANMODES", modeStr);
        }
        bits = modeStr.split(",", 5);
        if (bits.length < 4) {
            modeStr = sDefaultModes.toString();
            callErrorInfo(new ParserError(ParserError.ERROR_ERROR, "CHANMODES String not valid. Using default string of \"" + modeStr + "\"", getLastLine()));
            h005Info.put("CHANMODES", modeStr);
            bits = modeStr.split(",", 5);
        }

        // resetState
        chanModesOther.clear();
        chanModesBool.clear();
        nextKeyCMBool = 1;

        // List modes.
        for (int i = 0; i < bits[0].length(); ++i) {
            final Character cMode = bits[0].charAt(i);
            callDebugInfo(DEBUG_INFO, "Found List Mode: %c", cMode);
            if (!chanModesOther.containsKey(cMode)) {
                chanModesOther.put(cMode, MODE_LIST);
            }
        }

        // Param for Set and Unset.
        final Byte nBoth = MODE_SET + MODE_UNSET;
        for (int i = 0; i < bits[1].length(); ++i) {
            final Character cMode = bits[1].charAt(i);
            callDebugInfo(DEBUG_INFO, "Found Set/Unset Mode: %c", cMode);
            if (!chanModesOther.containsKey(cMode)) {
                chanModesOther.put(cMode, nBoth);
            }
        }

        // Param just for Set
        for (int i = 0; i < bits[2].length(); ++i) {
            final Character cMode = bits[2].charAt(i);
            callDebugInfo(DEBUG_INFO, "Found Set Only Mode: %c", cMode);
            if (!chanModesOther.containsKey(cMode)) {
                chanModesOther.put(cMode, MODE_SET);
            }
        }

        // Boolean Mode
        for (int i = 0; i < bits[3].length(); ++i) {
            final Character cMode = bits[3].charAt(i);
            callDebugInfo(DEBUG_INFO, "Found Boolean Mode: %c [%d]", cMode, nextKeyCMBool);
            if (!chanModesBool.containsKey(cMode)) {
                chanModesBool.put(cMode, nextKeyCMBool);
                nextKeyCMBool *= 2;
            }
        }
    }

    /** {@inheritDoc} */
    @Override
    public String getChannelUserModes() {
        if (h005Info.containsKey("PREFIXSTRING")) {
            return h005Info.get("PREFIXSTRING");
        } else {
            return "";
        }
    }

    /** {@inheritDoc} */
    @Override
    public String getBooleanChannelModes() {
        final char[] modes = new char[chanModesBool.size()];
        int i = 0;
        for (char mode : chanModesBool.keySet()) {
            modes[i++] = mode;
        }
        // Alphabetically sort the array
        Arrays.sort(modes);
        return new String(modes);
    }

    /** {@inheritDoc} */
    @Override
    public String getListChannelModes() {
        return getOtherModeString(MODE_LIST);
    }

    /** {@inheritDoc} */
    @Override
    public String getParameterChannelModes() {
        return getOtherModeString(MODE_SET);
    }

    /** {@inheritDoc} */
    @Override
    public String getDoubleParameterChannelModes() {
        return getOtherModeString((byte) (MODE_SET + MODE_UNSET));
    }

    /** {@inheritDoc} */
    @Override
    public String getChannelPrefixes() {
        if (chanPrefix.isEmpty()) {
            return "#&";
        }

        final StringBuilder builder = new StringBuilder(chanPrefix.size());

        synchronized (chanPrefix) {
            for (Character prefix : chanPrefix) {
                builder.append(prefix);
            }
        }

        return builder.toString();
    }

    /**
     * Get modes from hChanModesOther that have a specific value.
     * Modes are returned in alphabetical order
     *
     * @param value Value mode must have to be included
     * @return All the currently known Set-Unset modes
     */
    protected String getOtherModeString(final byte value) {
        final char[] modes = new char[chanModesOther.size()];
        Byte nTemp;
        int i = 0;
        for (char cTemp : chanModesOther.keySet()) {
            nTemp = chanModesOther.get(cTemp);
            if (nTemp == value) {
                modes[i++] = cTemp;
            }
        }
        // Alphabetically sort the array
        Arrays.sort(modes);
        return new String(modes).trim();
    }

    /** {@inheritDoc} */
    @Override
    public String getUserModes() {
        if (h005Info.containsKey("USERMODES")) {
            return h005Info.get("USERMODES");
        } else {
            return "";
        }
    }

    /**
     * Process USERMODES from 004.
     */
    protected void parseUserModes() {
        final String sDefaultModes = "nwdoi";
        String modeStr;
        if (h005Info.containsKey("USERMODES")) {
            modeStr = h005Info.get("USERMODES");
        } else {
            modeStr = sDefaultModes;
            h005Info.put("USERMODES", sDefaultModes);
        }

        // resetState
        userModes.clear();
        nNextKeyUser = 1;

        // Boolean Mode
        for (int i = 0; i < modeStr.length(); ++i) {
            final Character cMode = modeStr.charAt(i);
            callDebugInfo(DEBUG_INFO, "Found User Mode: %c [%d]", cMode, nNextKeyUser);
            if (!userModes.containsKey(cMode)) {
                userModes.put(cMode, nNextKeyUser);
                nNextKeyUser *= 2;
            }
        }
    }

    /**
     * Process CHANTYPES from 005.
     */
    protected void parseChanPrefix() {
        final String sDefaultModes = "#&";
        String modeStr;
        if (h005Info.containsKey("CHANTYPES")) {
            modeStr = h005Info.get("CHANTYPES");
        } else {
            modeStr = sDefaultModes;
            h005Info.put("CHANTYPES", sDefaultModes);
        }

        // resetState
        chanPrefix.clear();

        // Boolean Mode
        for (int i = 0; i < modeStr.length(); ++i) {
            final Character cMode = modeStr.charAt(i);
            callDebugInfo(DEBUG_INFO, "Found Chan Prefix: %c", cMode);
            if (!chanPrefix.contains(cMode)) {
                chanPrefix.add(cMode);
            }
        }
    }

    /**
     * Process PREFIX from 005.
     */
    public void parsePrefixModes() {
        final String sDefaultModes = "(ohv)@%+";
        String[] bits;
        String modeStr;
        if (h005Info.containsKey("PREFIX")) {
            modeStr = h005Info.get("PREFIX");
        } else {
            modeStr = sDefaultModes;
        }
        if (modeStr.substring(0, 1).equals("(")) {
            modeStr = modeStr.substring(1);
        } else {
            modeStr = sDefaultModes.substring(1);
            h005Info.put("PREFIX", sDefaultModes);
        }

        bits = modeStr.split("\\)", 2);
        if (bits.length != 2 || bits[0].length() != bits[1].length()) {
            modeStr = sDefaultModes;
            callErrorInfo(new ParserError(ParserError.ERROR_ERROR, "PREFIX String not valid. Using default string of \"" + modeStr + "\"", getLastLine()));
            h005Info.put("PREFIX", modeStr);
            modeStr = modeStr.substring(1);
            bits = modeStr.split("\\)", 2);
        }

        // resetState
        prefixModes.clear();
        prefixMap.clear();
        nextKeyPrefix = 1;

        for (int i = bits[0].length() - 1; i > -1; --i) {
            final Character cMode = bits[0].charAt(i);
            final Character cPrefix = bits[1].charAt(i);
            callDebugInfo(DEBUG_INFO, "Found Prefix Mode: %c => %c [%d]", cMode, cPrefix, nextKeyPrefix);
            if (!prefixModes.containsKey(cMode)) {
                prefixModes.put(cMode, nextKeyPrefix);
                prefixMap.put(cMode, cPrefix);
                prefixMap.put(cPrefix, cMode);
                nextKeyPrefix *= 2;
            }
        }

        h005Info.put("PREFIXSTRING", bits[0]);
    }

    /** {@inheritDoc} */
    @Override
    public void joinChannels(final ChannelJoinRequest... channels) {
        // We store a map from key->channels to allow intelligent joining of
        // channels using as few JOIN commands as needed.
        final Map<String, StringBuffer> joinMap = new HashMap<String, StringBuffer>();

        for (ChannelJoinRequest channel : channels) {
            // Make sure we have a list to put stuff in.
            StringBuffer list = joinMap.get(channel.getPassword());
            if (list == null) {
                list = new StringBuffer();
                joinMap.put(channel.getPassword(), list);
            }

            // Add the channel to the list. If the name is invalid and
            // autoprefix is off we will just skip this channel.
            if (!channel.getName().isEmpty()) {
                if (list.length() > 0) {
                    list.append(',');
                }
                if (!isValidChannelName(channel.getName())) {
                    if (h005Info.containsKey("CHANTYPES")) {
                        final String chantypes = h005Info.get("CHANTYPES");
                        if (chantypes.isEmpty()) {
                            list.append('#');
                        } else {
                            list.append(chantypes.charAt(0));
                        }
                    } else {
                        list.append('#');
                    }
                }
                list.append(channel.getName());
            }
        }

        for (Map.Entry<String, StringBuffer> entrySet : joinMap.entrySet()) {
            final String thisKey = entrySet.getKey();
            final String channelString = entrySet.getValue().toString();
            if (!channelString.isEmpty()) {
                if (thisKey == null || thisKey.isEmpty()) {
                    sendString("JOIN " + channelString);
                } else {
                    sendString("JOIN " + channelString + " " + thisKey);
                }
            }
        }
    }

    /**
     * Leave a Channel.
     *
     * @param channel Name of channel to part
     * @param reason Reason for leaving (Nothing sent if sReason is "")
     */
    public void partChannel(final String channel, final String reason) {
        if (getChannel(channel) == null) {
            return;
        }

        sendString("PART " + channel, reason);
    }

    /**
     * Set Nickname.
     *
     * @param nickname New nickname wanted.
     */
    public void setNickname(final String nickname) {
        if (getSocketState() == SocketState.OPEN) {
            if (!myself.isFake() && myself.getRealNickname().equals(nickname)) {
                return;
            }
            sendString("NICK " + nickname);
        } else {
            me.setNickname(nickname);
        }

        thinkNickname = nickname;
    }

    /** {@inheritDoc} */
    @Override
    public int getMaxLength(final String type, final String target) {
        // If my host is "nick!user@host" and we are sending "#Channel"
        // a "PRIVMSG" this will find the length of ":nick!user@host PRIVMSG #channel :"
        // and subtract it from the MAX_LINELENGTH. This should be sufficient in most cases.
        // Lint = the 2 ":" at the start and end and the 3 separating " "s
        int length = 0;
        if (type != null) {
            length += type.length();
        }
        if (target != null) {
            length += target.length();
        }
        return getMaxLength(length);
    }

    /**
     * Get the max length a message can be.
     *
     * @param length Length of stuff. (Ie "PRIVMSG"+"#Channel")
     * @return Max Length message should be.
     */
    public int getMaxLength(final int length) {
        final int lineLint = 5;
        if (myself.isFake()) {
            callErrorInfo(new ParserError(ParserError.ERROR_ERROR + ParserError.ERROR_USER, "getMaxLength() called, but I don't know who I am?", getLastLine()));
            return MAX_LINELENGTH - length - lineLint;
        } else {
            return MAX_LINELENGTH - myself.toString().length() - length - lineLint;
        }
    }

    /** {@inheritDoc} */
    @Override
    public int getMaxListModes(final char mode) {
        // MAXLIST=bdeI:50
        // MAXLIST=b:60,e:60,I:60
        // MAXBANS=30
        int result = -2;
        callDebugInfo(DEBUG_INFO, "Looking for maxlistmodes for: " + mode);
        // Try in MAXLIST
        if (h005Info.get("MAXLIST") != null) {
            if (h005Info.get("MAXBANS") == null) {
                result = 0;
            }
            final String maxlist = h005Info.get("MAXLIST");
            callDebugInfo(DEBUG_INFO, "Found maxlist (" + maxlist + ")");
            final String[] bits = maxlist.split(",");
            for (String bit : bits) {
                final String[] parts = bit.split(":", 2);
                callDebugInfo(DEBUG_INFO, "Bit: " + bit + " | parts.length = " + parts.length + " (" + parts[0] + " -> " + parts[0].indexOf(mode) + ")");
                if (parts.length == 2 && parts[0].indexOf(mode) > -1) {
                    callDebugInfo(DEBUG_INFO, "parts[0] = '" + parts[0] + "' | parts[1] = '" + parts[1] + "'");
                    try {
                        result = Integer.parseInt(parts[1]);
                        break;
                    } catch (NumberFormatException nfe) {
                        result = -1;
                    }
                }
            }
        }

        // If not in max list, try MAXBANS
        if (result == -2 && h005Info.get("MAXBANS") != null) {
            callDebugInfo(DEBUG_INFO, "Trying max bans");
            try {
                result = Integer.parseInt(h005Info.get("MAXBANS"));
            } catch (NumberFormatException nfe) {
                result = -1;
            }
        } else if (result == -2 && getServerType() == ServerType.WEIRCD) {
            result = 50;
        } else if (result == -2 && getServerType() == ServerType.OTHERNET) {
            result = 30;
        } else if (result == -2) {
            result = -1;
            callDebugInfo(DEBUG_INFO, "Failed");
            callErrorInfo(new ParserError(ParserError.ERROR_ERROR + ParserError.ERROR_USER, "Unable to discover max list modes.", getLastLine()));
        }
        callDebugInfo(DEBUG_INFO, "Result: " + result);
        return result;
    }

    /** {@inheritDoc} */
    @Override
    public void sendMessage(final String target, final String message) {
        if (target == null || message == null) {
            return;
        }
        if (target.isEmpty()) {
            return;
        }

        sendString("PRIVMSG " + target, message);
    }

    /** {@inheritDoc} */
    @Override
    public void sendNotice(final String target, final String message) {
        if (target == null || message == null) {
            return;
        }
        if (target.isEmpty()) {
            return;
        }

        sendString("NOTICE " + target, message);
    }

    /** {@inheritDoc} */
    @Override
    public void sendAction(final String target, final String message) {
        sendCTCP(target, "ACTION", message);
    }

    /** {@inheritDoc} */
    @Override
    public void sendCTCP(final String target, final String type, final String message) {
        if (target == null || message == null) {
            return;
        }
        if (target.isEmpty() || type.isEmpty()) {
            return;
        }
        final char char1 = (char) 1;
        sendString("PRIVMSG " + target, char1 + type.toUpperCase() + " " + message + char1);
    }

    /** {@inheritDoc} */
    @Override
    public void sendCTCPReply(final String target, final String type, final String message) {
        if (target == null || message == null) {
            return;
        }
        if (target.isEmpty() || type.isEmpty()) {
            return;
        }
        final char char1 = (char) 1;
        sendString("NOTICE " + target, char1 + type.toUpperCase() + " " + message + char1);
    }

    /** {@inheritDoc} */
    @Override
    public void requestGroupList(final String searchTerms) {
        sendString("LIST", searchTerms);
    }

    /**
     * Quit IRC.
     * This method will wait for the server to close the socket.
     *
     * @param reason Reason for quitting.
     */
    public void quit(final String reason) {
        sendString("QUIT", reason);
    }

    /** {@inheritDoc} */
    @Override
    public void disconnect(final String message) {
        super.disconnect(message);
        if (currentSocketState == SocketState.OPENING || currentSocketState == SocketState.OPEN) {
            currentSocketState = SocketState.CLOSING;
            if (got001) {
                quit(message);
            }
        }

        try {
            // SSLSockets try to close nicely and read data from the socket,
            // which seems to hang indefinitely in some circumstances. We don't
            // like indefinite hangs, so just close the underlying socket
            // direct.
            if (rawSocket != null) {
                rawSocket.close();
            }
        } catch (IOException e) {
            /* Do Nothing */
        } finally {
            if (currentSocketState != SocketState.CLOSED) {
                currentSocketState = SocketState.CLOSED;
                callSocketClosed();
            }
            resetState();
        }
    }

    /** {@inheritDoc}
     *
     * - Before channel prefixes are known (005/noMOTD/MOTDEnd), this checks
     *   that the first character is either #, &amp;, ! or +
     * - Assumes that any channel that is already known is valid, even if
     *   005 disagrees.
     */
    @Override
    public boolean isValidChannelName(final String name) {
        // Check sChannelName is not empty or null
        if (name == null || name.isEmpty()) {
            return false;
        }
        // Check its not ourself (PM recieved before 005)
        if (getStringConverter().equalsIgnoreCase(getMyNickname(), name)) {
            return false;
        }
        // Check if we are already on this channel
        if (getChannel(name) != null) {
            return true;
        }
        // Check if we know of any valid chan prefixes
        if (chanPrefix.isEmpty()) {
            // We don't. Lets check against RFC2811-Specified channel types
            final char first = name.charAt(0);
            return first == '#' || first == '&' || first == '!' || first == '+';
        }
        // Otherwise return true if:
        // Channel equals "0"
        // first character of the channel name is a valid channel prefix.
        return chanPrefix.contains(name.charAt(0)) || "0".equals(name);
    }

    /** {@inheritDoc} */
    @Override
    public boolean isUserSettable(final char mode) {
        String validmodes;
        if (h005Info.containsKey("USERCHANMODES")) {
            validmodes = h005Info.get("USERCHANMODES");
        } else {
            validmodes = "bklimnpstrc";
        }
        return validmodes.matches(".*" + mode + ".*");
    }

    /**
     * Get the 005 info.
     *
     * @return 005Info hashtable.
     */
    public Map<String, String> get005() {
        return h005Info;
    }

    /**
     * Get the ServerType for this IRCD.
     *
     * @return The ServerType for this IRCD.
     */
    public ServerType getServerType() {
        return ServerType.findServerType(h005Info.get("004IRCD"), networkName, h005Info.get("003IRCD"), h005Info.get("002IRCD"));
    }

    /** {@inheritDoc} */
    @Override
    public String getServerSoftware() {
        final String version = h005Info.get("004IRCD");
        return version == null ? "" : version;
    }

    /** {@inheritDoc} */
    @Override
    public String getServerSoftwareType() {
        return getServerType().getType();
    }

    /**
     * Get the value of checkServerPing.
     *
     * @return value of checkServerPing.
     * @see setCheckServerPing
     */
    public boolean getCheckServerPing() {
        return checkServerPing;
    }

    /**
     * Set the value of checkServerPing.
     *
     * @param newValue New value to use.
     * @see setCheckServerPing
     */
    public void setCheckServerPing(final boolean newValue) {
        checkServerPing = newValue;
        if (checkServerPing) {
            startPingTimer();
        } else {
            stopPingTimer();
        }
    }

    /** {@inheritDoc} */
    @Override
    public void setEncoder(final Encoder encoder) {
        this.encoder = encoder;
    }

    /** {@inheritDoc} */
    @Override
    public void setPingTimerInterval(final long newValue) {
        super.setPingTimerInterval(newValue);

        startPingTimer();
    }

    /**
     * Start the pingTimer.
     */
    protected void startPingTimer() {
        pingTimerSem.acquireUninterruptibly();

        try {
            setPingNeeded(false);

            if (pingTimer != null) {
                pingTimer.cancel();
            }

            pingTimer = new Timer("IRCParser pingTimer");
            pingTimer.schedule(new PingTimer(this, pingTimer), 0, getPingTimerInterval());
            pingCountDown = 1;
        } finally {
            pingTimerSem.release();
        }
    }

    /**
     * Stop the pingTimer.
     */
    protected void stopPingTimer() {
        pingTimerSem.acquireUninterruptibly();
        if (pingTimer != null) {
            pingTimer.cancel();
            pingTimer = null;
        }
        pingTimerSem.release();
    }

    /**
     * This is called when the ping Timer has been executed.
     * As the timer is restarted on every incomming message, this will only be
     * called when there has been no incomming line for 10 seconds.
     *
     * @param timer The timer that called this.
     */
    protected void pingTimerTask(final Timer timer) {
        // If user no longer wants server ping to be checked, or the socket is
        // closed then cancel the time and do nothing else.
        if (!getCheckServerPing() || getSocketState() != SocketState.OPEN) {
            pingTimerSem.acquireUninterruptibly();
            if (pingTimer != null && pingTimer.equals(timer)) {
                pingTimer.cancel();
            }
            pingTimerSem.release();

            return;
        }
        if (getPingNeeded()) {
            if (!callPingFailed()) {
                pingTimerSem.acquireUninterruptibly();

                if (pingTimer != null && pingTimer.equals(timer)) {
                    pingTimer.cancel();
                }
                pingTimerSem.release();

                disconnect("Server not responding.");
            }
        } else {
            --pingCountDown;
            if (pingCountDown < 1) {
                pingTime = System.currentTimeMillis();
                setPingNeeded(true);
                pingCountDown = getPingTimerFraction();
                lastPingValue = String.valueOf(System.currentTimeMillis());
                if (sendString("PING " + lastPingValue, QueuePriority.HIGH)) {
                    callPingSent();
                }
            }
        }
    }

    /** {@inheritDoc} */
    @Override
    public long getServerLatency() {
        return serverLag;
    }

    /**
     * Updates the name of the server that this parser is connected to.
     *
     * @param serverName The discovered server name
     */
    protected void updateServerName(final String serverName) {
        setServerName(serverName);
    }

    /**
     * Get the current server lag.
     *
     * @param actualTime if True the value returned will be the actual time the ping was sent
     *                   else it will be the amount of time sinse the last ping was sent.
     * @return Time last ping was sent
     */
    public long getPingTime(final boolean actualTime) {
        if (actualTime) {
            return pingTime;
        } else {
            return System.currentTimeMillis() - pingTime;
        }
    }

    /** {@inheritDoc} */
    @Override
    public long getPingTime() {
        return getPingTime(false);
    }

    /**
     * Set if a ping is needed or not.
     *
     * @param newStatus new value to set pingNeeded to.
     */
    private void setPingNeeded(final boolean newStatus) {
        pingNeeded.set(newStatus);
    }

    /**
     * Get if a ping is needed or not.
     *
     * @return value of pingNeeded.
     */
    private boolean getPingNeeded() {
        return pingNeeded.get();
    }

    /** {@inheritDoc} */
    @Override
    public IRCClientInfo getLocalClient() {
        return myself;
    }

    /**
     * Get the current nickname.
     * After 001 this returns the exact same as getLocalClient().getRealNickname();
     * Before 001 it returns the nickname that the parser Thinks it has.
     *
     * @return Current nickname.
     */
    public String getMyNickname() {
        if (myself.isFake()) {
            return thinkNickname;
        } else {
            return myself.getRealNickname();
        }
    }

    /**
     * Retrieves the local user information that this parser was configured
     * with.
     *
     * @return This parser's local user configuration
     */
    public MyInfo getMyInfo() {
        return me;
    }

    /**
     * Get the current username (Specified in MyInfo on construction).
     * Get the username given in MyInfo
     *
     * @return My username.
     */
    public String getMyUsername() {
        return me.getUsername();
    }

    /**
     * Add a client to the ClientList.
     *
     * @param client Client to add
     */
    public void addClient(final IRCClientInfo client) {
        clientList.put(getStringConverter().toLowerCase(client.getRealNickname()), client);
    }

    /**
     * Remove a client from the ClientList.
     * This WILL NOT allow cMyself to be removed from the list.
     *
     * @param client Client to remove
     */
    public void removeClient(final IRCClientInfo client) {
        if (client != myself) {
            forceRemoveClient(client);
        }
    }

    /**
     * Remove a client from the ClientList.
     * This WILL allow cMyself to be removed from the list
     *
     * @param client Client to remove
     */
    protected void forceRemoveClient(final IRCClientInfo client) {
        clientList.remove(getStringConverter().toLowerCase(client.getRealNickname()));
    }

    /**
     * Get the number of known clients.
     *
     * @return Count of known clients
     */
    public int knownClients() {
        return clientList.size();
    }

    /**
     * Get the known clients as a collection.
     *
     * @return Known clients as a collection
     */
    public Collection<IRCClientInfo> getClients() {
        return clientList.values();
    }

    /**
     * Clear the client list.
     */
    public void clearClients() {
        clientList.clear();
        addClient(getLocalClient());
    }

    /**
     * Add a channel to the ChannelList.
     *
     * @param channel Channel to add
     */
    public void addChannel(final IRCChannelInfo channel) {
        synchronized (channelList) {
            channelList.put(getStringConverter().toLowerCase(channel.getName()), channel);
        }
    }

    /**
     * Remove a channel from the ChannelList.
     *
     * @param channel Channel to remove
     */
    public void removeChannel(final IRCChannelInfo channel) {
        synchronized (channelList) {
            channelList.remove(getStringConverter().toLowerCase(channel.getName()));
        }
    }

    /**
     * Get the number of known channel.
     *
     * @return Count of known channel
     */
    public int knownChannels() {
        synchronized (channelList) {
            return channelList.size();
        }
    }

    /** {@inheritDoc} */
    @Override
    public Collection<IRCChannelInfo> getChannels() {
        synchronized (channelList) {
            return channelList.values();
        }
    }

    /**
     * Clear the channel list.
     */
    public void clearChannels() {
        synchronized (channelList) {
            channelList.clear();
        }
    }

    /** {@inheritDoc} */
    @Override
    public String[] parseHostmask(final String hostmask) {
        return IRCClientInfo.parseHostFull(hostmask);
    }

    /** {@inheritDoc} */
    @Override
    public int getMaxTopicLength() {
        if (h005Info.containsKey("TOPICLEN")) {
            try {
                return Integer.parseInt(h005Info.get("TOPICLEN"));
            } catch (NumberFormatException ex) {
                // Do nothing
            }
        }

        return 0;
    }

    /** {@inheritDoc} */
    @Override
    public int getMaxLength() {
        return MAX_LINELENGTH;
    }

    /** {@inheritDoc} */
    @Override
    public void setCompositionState(final String host, final CompositionState state) {
        // Do nothing
    }
}
