/*
 * %CopyrightBegin%
 *
 * Copyright Ericsson AB 2000-2017. 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.
 *
 * %CopyrightEnd%
 */

package com.ericsson.otp.erlang;

import java.io.IOException;
import java.io.OutputStream;
import java.util.Random;

/**
 * Maintains a connection between a Java process and a remote Erlang, Java or C
 * node. The object maintains connection state and allows data to be sent to and
 * received from the peer.
 *
 * <p>
 * This abstract class provides the necessary methods to maintain the actual
 * connection and encode the messages and headers in the proper format according
 * to the Erlang distribution protocol. Subclasses can use these methods to
 * provide a more or less transparent communication channel as desired.
 * </p>
 *
 * <p>
 * Note that no receive methods are provided. Subclasses must provide methods
 * for message delivery, and may implement their own receive methods.
 * <p>
 *
 * <p>
 * If an exception occurs in any of the methods in this class, the connection
 * will be closed and must be reopened in order to resume communication with the
 * peer. This will be indicated to the subclass by passing the exception to its
 * delivery() method.
 * </p>
 *
 * <p>
 * The System property OtpConnection.trace can be used to change the initial
 * trace level setting for all connections. Normally the initial trace level is
 * 0 and connections are not traced unless {@link #setTraceLevel
 * setTraceLevel()} is used to change the setting for a particular connection.
 * OtpConnection.trace can be used to turn on tracing by default for all
 * connections.
 * </p>
 */
public abstract class AbstractConnection extends Thread {
    protected static final int headerLen = 2048; // more than enough

    protected static final byte passThrough = (byte) 0x70;
    protected static final byte version = (byte) 0x83;

    // Erlang message header tags
    protected static final int linkTag = 1;
    protected static final int sendTag = 2;
    protected static final int exitTag = 3;
    protected static final int unlinkTag = 4;
    protected static final int regSendTag = 6;
    protected static final int groupLeaderTag = 7;
    protected static final int exit2Tag = 8;

    protected static final int sendTTTag = 12;
    protected static final int exitTTTag = 13;
    protected static final int regSendTTTag = 16;
    protected static final int exit2TTTag = 18;
    protected static final int unlinkIdTag = 35;
    protected static final int unlinkIdAckTag = 36;

    // MD5 challenge messsage tags
    protected static final int ChallengeReply = 'r';
    protected static final int ChallengeAck = 'a';
    protected static final int ChallengeStatus = 's';

    private volatile boolean done = false;

    protected boolean connected = false; // connection status
    protected OtpTransport socket; // communication channel
    protected OtpPeer peer; // who are we connected to
    protected OtpLocalNode localNode; // this nodes id
    String name; // local name of this connection

    protected boolean cookieOk = false; // already checked the cookie for this
    // connection
    protected boolean sendCookie = true; // Send cookies in messages?

    // tracelevel constants
    protected int traceLevel = 0;

    protected static int defaultLevel = 0;
    protected static int sendThreshold = 1;
    protected static int ctrlThreshold = 2;
    protected static int handshakeThreshold = 3;

    protected static Random random = null;

    private int flags = 0;

    static {
        // trace this connection?
        final String trace = System.getProperties().getProperty(
                "OtpConnection.trace");
        try {
            if (trace != null) {
                defaultLevel = Integer.valueOf(trace).intValue();
            }
        } catch (final NumberFormatException e) {
            defaultLevel = 0;
        }
        random = new Random();
    }

    // private AbstractConnection() {
    // }

    /**
     * Accept an incoming connection from a remote node. Used by
     * {@link OtpSelf#accept() OtpSelf.accept()} to create a connection based on
     * data received when handshaking with the peer node, when the remote node
     * is the connection initiator.
     *
     * @exception java.io.IOException
     *                if it was not possible to connect to the peer.
     *
     * @exception OtpAuthException
     *                if handshake resulted in an authentication error
     */
    protected AbstractConnection(final OtpLocalNode self, final OtpTransport s)
            throws IOException, OtpAuthException {
        localNode = self;
        peer = new OtpPeer(self.transportFactory);
        socket = s;

        traceLevel = defaultLevel;
        setDaemon(true);

        if (traceLevel >= handshakeThreshold) {
            System.out.println("<- ACCEPT FROM " + s);
        }
        doAccept();
        name = peer.node();
    }

    /**
     * Intiate and open a connection to a remote node.
     *
     * @exception java.io.IOException
     *                if it was not possible to connect to the peer.
     *
     * @exception OtpAuthException
     *                if handshake resulted in an authentication error.
     */
    protected AbstractConnection(final OtpLocalNode self, final OtpPeer other)
            throws IOException, OtpAuthException {
        peer = other;
        localNode = self;
        socket = null;
        int port;

        traceLevel = defaultLevel;
        setDaemon(true);

        // now get a connection between the two...
        port = OtpEpmd.lookupPort(peer);
        if (port == 0)
            throw new IOException("No remote node found - cannot connect");

        // now find highest common dist value
        if (peer.proto != self.proto || self.distHigh < peer.distLow
                || self.distLow > peer.distHigh) {
            throw new IOException("No common protocol found - cannot connect");
        }

        // highest common version: min(peer.distHigh, self.distHigh)
        peer.distChoose = peer.distHigh > self.distHigh ? self.distHigh
                : peer.distHigh;

        doConnect(port);

        name = peer.node();
        connected = true;
    }

    /**
     * Deliver communication exceptions to the recipient.
     */
    public abstract void deliver(Exception e);

    /**
     * Deliver messages to the recipient.
     */
    public abstract void deliver(OtpMsg msg);

    /**
     * Send a pre-encoded message to a named process on a remote node.
     *
     * @param dest
     *            the name of the remote process.
     * @param payload
     *            the encoded message to send.
     *
     * @exception java.io.IOException
     *                if the connection is not active or a communication error
     *                occurs.
     */
    protected void sendBuf(final OtpErlangPid from, final String dest,
            final OtpOutputStream payload) throws IOException {
        if (!connected) {
            throw new IOException("Not connected");
        }
        @SuppressWarnings("resource")
        final OtpOutputStream header = new OtpOutputStream(headerLen);

        // preamble: 4 byte length + "passthrough" tag + version
        header.write4BE(0); // reserve space for length
        header.write1(passThrough);
        header.write1(version);

        // header info
        header.write_tuple_head(4);
        header.write_long(regSendTag);
        header.write_any(from);
        if (sendCookie) {
            header.write_atom(localNode.cookie());
        } else {
            header.write_atom("");
        }
        header.write_atom(dest);

        // version for payload
        header.write1(version);

        // fix up length in preamble
        header.poke4BE(0, header.size() + payload.size() - 4);

        do_send(header, payload);
    }

    /**
     * Send a pre-encoded message to a process on a remote node.
     *
     * @param dest
     *            the Erlang PID of the remote process.
     * @param payload
     *            the encoded message to send.
     *
     * @exception java.io.IOException
     *                if the connection is not active or a communication error
     *                occurs.
     */
    protected void sendBuf(final OtpErlangPid from, final OtpErlangPid dest,
            final OtpOutputStream payload) throws IOException {
        if (!connected) {
            throw new IOException("Not connected");
        }
        @SuppressWarnings("resource")
        final OtpOutputStream header = new OtpOutputStream(headerLen);

        // preamble: 4 byte length + "passthrough" tag + version
        header.write4BE(0); // reserve space for length
        header.write1(passThrough);
        header.write1(version);

        // header info
        header.write_tuple_head(3);
        header.write_long(sendTag);
        if (sendCookie) {
            header.write_atom(localNode.cookie());
        } else {
            header.write_atom("");
        }
        header.write_any(dest);

        // version for payload
        header.write1(version);

        // fix up length in preamble
        header.poke4BE(0, header.size() + payload.size() - 4);

        do_send(header, payload);
    }

    /*
     * Send an auth error to peer because he sent a bad cookie. The auth error
     * uses his cookie (not revealing ours). This is just like send_reg
     * otherwise
     */
    private void cookieError(final OtpLocalNode local,
            final OtpErlangAtom cookie) throws OtpAuthException {
        try {
            @SuppressWarnings("resource")
            final OtpOutputStream header = new OtpOutputStream(headerLen);

            // preamble: 4 byte length + "passthrough" tag + version
            header.write4BE(0); // reserve space for length
            header.write1(passThrough);
            header.write1(version);

            header.write_tuple_head(4);
            header.write_long(regSendTag);
            header.write_any(local.createPid()); // disposable pid
            header.write_atom(cookie.atomValue()); // important: his cookie,
            // not mine...
            header.write_atom("auth");

            // version for payload
            header.write1(version);

            // the payload

            // the no_auth message (copied from Erlang) Don't change this
            // (Erlang will crash)
            // {$gen_cast, {print, "~n** Unauthorized cookie ~w **~n",
            // [foo@aule]}}
            final OtpErlangObject[] msg = new OtpErlangObject[2];
            final OtpErlangObject[] msgbody = new OtpErlangObject[3];

            msgbody[0] = new OtpErlangAtom("print");
            msgbody[1] = new OtpErlangString("~n** Bad cookie sent to " + local
                    + " **~n");
            // Erlang will crash and burn if there is no third argument here...
            msgbody[2] = new OtpErlangList(); // empty list

            msg[0] = new OtpErlangAtom("$gen_cast");
            msg[1] = new OtpErlangTuple(msgbody);

            @SuppressWarnings("resource")
            final OtpOutputStream payload = new OtpOutputStream(
                    new OtpErlangTuple(msg));

            // fix up length in preamble
            header.poke4BE(0, header.size() + payload.size() - 4);

            try {
                do_send(header, payload);
            } catch (final IOException e) {
            } // ignore
        } finally {
            close();
        }
        throw new OtpAuthException("Remote cookie not authorized: "
                + cookie.atomValue());
    }

    // link to pid

    /**
     *
     * Send link signal to remote process.
     *
     * @param dest
     *            the Erlang PID of the remote process.
     *
     * @exception java.io.IOException
     *                if the connection is not active or a communication error
     *                occurs.
     */
    protected void sendLink(final OtpErlangPid from, final OtpErlangPid dest)
            throws IOException {
        if (!connected) {
            throw new IOException("Not connected");
        }
        @SuppressWarnings("resource")
        final OtpOutputStream header = new OtpOutputStream(headerLen);

        // preamble: 4 byte length + "passthrough" tag
        header.write4BE(0); // reserve space for length
        header.write1(passThrough);
        header.write1(version);

        // header
        header.write_tuple_head(3);
        header.write_long(linkTag);
        header.write_any(from);
        header.write_any(dest);

        // fix up length in preamble
        header.poke4BE(0, header.size() - 4);

        do_send(header);
    }

    /**
     *
     * Send unlink signal to remote process.
     *
     * @param dest
     *            the Erlang PID of the remote process.
     *
     * @exception java.io.IOException
     *                if the connection is not active or a communication error
     *                occurs.
     */
    protected void sendUnlink(final OtpErlangPid from, final OtpErlangPid dest,
                              long unlink_id)
            throws IOException {
        if (!connected) {
            throw new IOException("Not connected");
        }
        @SuppressWarnings("resource")
        final OtpOutputStream header = new OtpOutputStream(headerLen);

        // preamble: 4 byte length + "passthrough" tag
        header.write4BE(0); // reserve space for length
        header.write1(passThrough);
        header.write1(version);

        if ((peer.flags & AbstractNode.dFlagUnlinkId) != 0) {
            // header
            header.write_tuple_head(4);
            header.write_long(unlinkIdTag);
            header.write_long(unlink_id);
            header.write_any(from);
            header.write_any(dest);
        }
        else {
            /*
             * A node that isn't capable of talking the new link protocol.
             *
             * Send an old unlink op, and send ourselves an unlink-ack. We may
             * end up in an inconsistent state as we could before the new link
             * protocol was introduced...
             */
            // header
            header.write_tuple_head(3);
            header.write_long(unlinkTag);
            header.write_any(from);
            header.write_any(dest);
            deliver(new OtpMsg(unlinkIdAckTag, dest, from, unlink_id));
        }

        // fix up length in preamble
        header.poke4BE(0, header.size() - 4);

        do_send(header);
    }

    /**
     * Send unlink acknowledgment signal to remote process.
     *
     * @param dest
     *            the Erlang PID of the remote process.
     *
     * @exception java.io.IOException
     *                if the connection is not active or a communication error
     *                occurs.
     */
    protected void sendUnlinkAck(final OtpErlangPid from, final OtpErlangPid dest,
                                 long unlink_id)
            throws IOException {
        if (!connected) {
            throw new IOException("Not connected");
        }
        if ((peer.flags & AbstractNode.dFlagUnlinkId) != 0) {
            @SuppressWarnings("resource")
            final OtpOutputStream header = new OtpOutputStream(headerLen);

            // preamble: 4 byte length + "passthrough" tag
            header.write4BE(0); // reserve space for length
            header.write1(passThrough);
            header.write1(version);

            // header
            header.write_tuple_head(4);
            header.write_long(unlinkIdAckTag);
            header.write_long(unlink_id);
            header.write_any(from);
            header.write_any(dest);
            // fix up length in preamble
            header.poke4BE(0, header.size() - 4);

            do_send(header);
        }

    }

    /* used internally when "processes" terminate */
    protected void sendExit(final OtpErlangPid from, final OtpErlangPid dest,
            final OtpErlangObject reason) throws IOException {
        sendExit(exitTag, from, dest, reason);
    }

    /**
     * Send an exit signal to a remote process.
     *
     * @param dest
     *            the Erlang PID of the remote process.
     * @param reason
     *            an Erlang term describing the exit reason.
     *
     * @exception java.io.IOException
     *                if the connection is not active or a communication error
     *                occurs.
     */
    protected void sendExit2(final OtpErlangPid from, final OtpErlangPid dest,
            final OtpErlangObject reason) throws IOException {
        sendExit(exit2Tag, from, dest, reason);
    }

    private void sendExit(final int tag, final OtpErlangPid from,
            final OtpErlangPid dest, final OtpErlangObject reason)
            throws IOException {
        if (!connected) {
            throw new IOException("Not connected");
        }
        @SuppressWarnings("resource")
        final OtpOutputStream header = new OtpOutputStream(headerLen);

        // preamble: 4 byte length + "passthrough" tag
        header.write4BE(0); // reserve space for length
        header.write1(passThrough);
        header.write1(version);

        // header
        header.write_tuple_head(4);
        header.write_long(tag);
        header.write_any(from);
        header.write_any(dest);
        header.write_any(reason);

        // fix up length in preamble
        header.poke4BE(0, header.size() - 4);

        do_send(header);
    }

    @SuppressWarnings("resource")
    @Override
    public void run() {
        if (!connected) {
            deliver(new IOException("Not connected"));
            return;
        }

        final byte[] lbuf = new byte[4];
        OtpInputStream ibuf;
        OtpErlangObject traceobj;
        int len;
        final byte[] tock = { 0, 0, 0, 0 };

        try {
            receive_loop: while (!done) {
                // don't return until we get a real message
                // or a failure of some kind (e.g. EXIT)
                // read length and read buffer must be atomic!
                do {
                    // read 4 bytes - get length of incoming packet
                    // socket.getInputStream().read(lbuf);
                    readSock(socket, lbuf);
                    ibuf = new OtpInputStream(lbuf, flags);
                    len = ibuf.read4BE();

                    // received tick? send tock!
                    if (len == 0) {
                        synchronized (this) {
                            OutputStream out = socket.getOutputStream();
                            out.write(tock);
                            out.flush();
                        }
                    }

                } while (len == 0); // tick_loop

                // got a real message (maybe) - read len bytes
                final byte[] tmpbuf = new byte[len];
                // i = socket.getInputStream().read(tmpbuf);
                readSock(socket, tmpbuf);
                ibuf.close();
                ibuf = new OtpInputStream(tmpbuf, flags);

                if (ibuf.read1() != passThrough) {
                    break receive_loop;
                }

                // got a real message (really)
                OtpErlangObject reason = null;
                OtpErlangAtom cookie = null;
                OtpErlangObject tmp = null;
                OtpErlangTuple head = null;
                OtpErlangAtom toName;
                OtpErlangPid to;
                OtpErlangPid from;
                int tag;

                // decode the header
                tmp = ibuf.read_any();
                if (!(tmp instanceof OtpErlangTuple)) {
                    break receive_loop;
                }

                head = (OtpErlangTuple) tmp;
                if (!(head.elementAt(0) instanceof OtpErlangLong)) {
                    break receive_loop;
                }

                // lets see what kind of message this is
                tag = (int) ((OtpErlangLong) head.elementAt(0)).longValue();

                switch (tag) {
                case sendTag: // { SEND, Cookie, ToPid }
                case sendTTTag: // { SEND, Cookie, ToPid, TraceToken }
                    if (!cookieOk) {
                        // we only check this once, he can send us bad cookies
                        // later if he likes
                        if (!(head.elementAt(1) instanceof OtpErlangAtom)) {
                            break receive_loop;
                        }
                        cookie = (OtpErlangAtom) head.elementAt(1);
                        if (sendCookie) {
                            if (!cookie.atomValue().equals(localNode.cookie())) {
                                cookieError(localNode, cookie);
                            }
                        } else {
                            if (!cookie.atomValue().equals("")) {
                                cookieError(localNode, cookie);
                            }
                        }
                        cookieOk = true;
                    }

                    if (traceLevel >= sendThreshold) {
                        System.out.println("<- " + headerType(head) + " "
                                + head);

                        /* show received payload too */
                        ibuf.mark(0);
                        traceobj = ibuf.read_any();

                        if (traceobj != null) {
                            System.out.println("   " + traceobj);
                        } else {
                            System.out.println("   (null)");
                        }
                        ibuf.reset();
                    }

                    to = (OtpErlangPid) head.elementAt(2);

                    deliver(new OtpMsg(to, ibuf));
                    break;

                case regSendTag: // { REG_SEND, FromPid, Cookie, ToName }
                case regSendTTTag: // { REG_SEND, FromPid, Cookie, ToName,
                    // TraceToken }
                    if (!cookieOk) {
                        // we only check this once, he can send us bad cookies
                        // later if he likes
                        if (!(head.elementAt(2) instanceof OtpErlangAtom)) {
                            break receive_loop;
                        }
                        cookie = (OtpErlangAtom) head.elementAt(2);
                        if (sendCookie) {
                            if (!cookie.atomValue().equals(localNode.cookie())) {
                                cookieError(localNode, cookie);
                            }
                        } else {
                            if (!cookie.atomValue().equals("")) {
                                cookieError(localNode, cookie);
                            }
                        }
                        cookieOk = true;
                    }

                    if (traceLevel >= sendThreshold) {
                        System.out.println("<- " + headerType(head) + " "
                                + head);

                        /* show received payload too */
                        ibuf.mark(0);
                        traceobj = ibuf.read_any();

                        if (traceobj != null) {
                            System.out.println("   " + traceobj);
                        } else {
                            System.out.println("   (null)");
                        }
                        ibuf.reset();
                    }

                    from = (OtpErlangPid) head.elementAt(1);
                    toName = (OtpErlangAtom) head.elementAt(3);

                    deliver(new OtpMsg(from, toName.atomValue(), ibuf));
                    break;

                case exitTag: // { EXIT, FromPid, ToPid, Reason }
                case exit2Tag: // { EXIT2, FromPid, ToPid, Reason }
                    if (head.elementAt(3) == null) {
                        break receive_loop;
                    }
                    if (traceLevel >= ctrlThreshold) {
                        System.out.println("<- " + headerType(head) + " "
                                + head);
                    }

                    from = (OtpErlangPid) head.elementAt(1);
                    to = (OtpErlangPid) head.elementAt(2);
                    reason = head.elementAt(3);

                    deliver(new OtpMsg(tag, from, to, reason));
                    break;

                case exitTTTag: // { EXIT, FromPid, ToPid, TraceToken, Reason }
                case exit2TTTag: // { EXIT2, FromPid, ToPid, TraceToken,
                    // Reason
                    // }
                    // as above, but bifferent element number
                    if (head.elementAt(4) == null) {
                        break receive_loop;
                    }
                    if (traceLevel >= ctrlThreshold) {
                        System.out.println("<- " + headerType(head) + " "
                                + head);
                    }

                    from = (OtpErlangPid) head.elementAt(1);
                    to = (OtpErlangPid) head.elementAt(2);
                    reason = head.elementAt(4);

                    deliver(new OtpMsg(tag, from, to, reason));
                    break;

                case linkTag: // { LINK, FromPid, ToPid}
                case unlinkTag: // { UNLINK, FromPid, ToPid}
                    if (traceLevel >= ctrlThreshold) {
                        System.out.println("<- " + headerType(head) + " "
                                + head);
                    }

                    from = (OtpErlangPid) head.elementAt(1);
                    to = (OtpErlangPid) head.elementAt(2);

                    deliver(new OtpMsg(tag, from, to, 0));
                    break;

                case unlinkIdTag: // { UNLINK_ID, UnlinkId, FromPid, ToPid}
                case unlinkIdAckTag: // { UNLINK_ID_Ack, UnlinkId, FromPid, ToPid}
                    if (traceLevel >= ctrlThreshold) {
                        System.out.println("<- " + headerType(head) + " "
                                + head);
                    }

                    long unlink_id = ((OtpErlangLong) head.elementAt(1)).longValue();
                    from = (OtpErlangPid) head.elementAt(2);
                    to = (OtpErlangPid) head.elementAt(3);

                    deliver(new OtpMsg(tag, from, to, unlink_id));
                    break;

                // absolutely no idea what to do with these, so we ignore
                // them...
                case groupLeaderTag: // { GROUPLEADER, FromPid, ToPid}
                    // (just show trace)
                    if (traceLevel >= ctrlThreshold) {
                        System.out.println("<- " + headerType(head) + " "
                                + head);
                    }
                    break;

                default:
                    // garbage?
                    break receive_loop;
                }
            } // end receive_loop

            // this section reachable only with break
            // we have received garbage from peer
            deliver(new OtpErlangExit("Remote is sending garbage"));

        } // try

        catch (final OtpAuthException e) {
            deliver(e);
        } catch (final OtpErlangDecodeException e) {
            deliver(new OtpErlangExit("Remote is sending garbage"));
        } catch (final IOException e) {
            deliver(new OtpErlangExit("Remote has closed connection"));
        } finally {
            close();
        }
    }

    /**
     * <p>
     * Set the trace level for this connection. Normally tracing is off by
     * default unless System property OtpConnection.trace was set.
     * </p>
     *
     * <p>
     * The following levels are valid: 0 turns off tracing completely, 1 shows
     * ordinary send and receive messages, 2 shows control messages such as link
     * and unlink, 3 shows handshaking at connection setup, and 4 shows
     * communication with Epmd. Each level includes the information shown by the
     * lower ones.
     * </p>
     *
     * @param level
     *            the level to set.
     *
     * @return the previous trace level.
     */
    public int setTraceLevel(final int level) {
        final int oldLevel = traceLevel;

        // pin the value
        int theLevel = level;
        if (level < 0) {
            theLevel = 0;
        } else if (level > 4) {
            theLevel = 4;
        }

        traceLevel = theLevel;

        return oldLevel;
    }

    /**
     * Get the trace level for this connection.
     *
     * @return the current trace level.
     */
    public int getTraceLevel() {
        return traceLevel;
    }

    /**
     * Close the connection to the remote node.
     */
    public void close() {
        done = true;
        connected = false;
        synchronized (this) {
            try {
                if (socket != null) {
                    if (traceLevel >= ctrlThreshold) {
                        System.out.println("-> CLOSE");
                    }
                    socket.close();
                }
            } catch (final IOException e) { /* ignore socket close errors */
            } finally {
                socket = null;
            }
        }
    }

    @Override
    protected void finalize() {
        close();
    }

    /**
     * Determine if the connection is still alive. Note that this method only
     * reports the status of the connection, and that it is possible that there
     * are unread messages waiting in the receive queue.
     *
     * @return true if the connection is alive.
     */
    public boolean isConnected() {
        return connected;
    }

    // used by send and send_reg (message types with payload)
    protected synchronized void do_send(final OtpOutputStream header,
            final OtpOutputStream payload) throws IOException {
        try {
            if (traceLevel >= sendThreshold) {
                // Need to decode header and output buffer to show trace
                // message!
                // First make OtpInputStream, then decode.
                try {
                    final OtpErlangObject h = header.getOtpInputStream(5)
                            .read_any();
                    System.out.println("-> " + headerType(h) + " " + h);

                    OtpErlangObject o = payload.getOtpInputStream(0).read_any();
                    System.out.println("   " + o);
                    o = null;
                } catch (final OtpErlangDecodeException e) {
                    System.out.println("   " + "can't decode output buffer:"
                            + e);
                }
            }

            // group flush op in favour of possible ssh-tunneled stream
            OutputStream out = socket.getOutputStream();
            header.writeTo(out);
            payload.writeTo(out);
            out.flush();
        } catch (final IOException e) {
            close();
            throw e;
        }
    }

    // used by the other message types
    protected synchronized void do_send(final OtpOutputStream header)
            throws IOException {
        try {
            if (traceLevel >= ctrlThreshold) {
                try {
                    final OtpErlangObject h = header.getOtpInputStream(5)
                            .read_any();
                    System.out.println("-> " + headerType(h) + " " + h);
                } catch (final OtpErlangDecodeException e) {
                    System.out.println("   " + "can't decode output buffer: "
                            + e);
                }
            }
            header.writeToAndFlush(socket.getOutputStream());
        } catch (final IOException e) {
            close();
            throw e;
        }
    }

    protected String headerType(final OtpErlangObject h) {
        int tag = -1;

        if (h instanceof OtpErlangTuple) {
            tag = (int) ((OtpErlangLong) ((OtpErlangTuple) h).elementAt(0))
                    .longValue();
        }

        switch (tag) {
        case linkTag:
            return "LINK";

        case sendTag:
            return "SEND";

        case exitTag:
            return "EXIT";

        case unlinkTag:
            return "UNLINK";

        case unlinkIdTag:
            return "UNLINK_ID";

        case unlinkIdAckTag:
            return "UNLINK_ID_ACK";

        case regSendTag:
            return "REG_SEND";

        case groupLeaderTag:
            return "GROUP_LEADER";

        case exit2Tag:
            return "EXIT2";

        case sendTTTag:
            return "SEND_TT";

        case exitTTTag:
            return "EXIT_TT";

        case regSendTTTag:
            return "REG_SEND_TT";

        case exit2TTTag:
            return "EXIT2_TT";
        }

        return "(unknown type)";
    }

    /* this method now throws exception if we don't get full read */
    protected int readSock(final OtpTransport s, final byte[] b)
            throws IOException {
        int got = 0;
        final int len = b.length;
        int i;

        synchronized (this) {
            if (s == null) {
                throw new IOException("expected " + len
                        + " bytes, socket was closed");
            }
        }

        while (got < len) {
            i = s.getInputStream().read(b, got, len - got);

            if (i < 0) {
                throw new IOException("expected " + len
                        + " bytes, got EOF after " + got + " bytes");
            } else if (i == 0 && len != 0) {
                /*
                 * This is a corner case. According to
                 * http://java.sun.com/j2se/1.4.2/docs/api/ class InputStream
                 * is.read(,,l) can only return 0 if l==0. In other words it
                 * should not happen, but apparently did.
                 */
                throw new IOException("Remote connection closed");
            } else {
                got += i;
            }
        }
        return got;
    }

    protected void doAccept() throws IOException, OtpAuthException {
        final int send_name_tag = recvName(peer);
        try {
            sendStatus("ok");
            final int our_challenge = genChallenge();
            sendChallenge(peer.flags, localNode.flags, our_challenge);
            recvComplement(send_name_tag);
            final int her_challenge = recvChallengeReply(our_challenge);
            final byte[] our_digest = genDigest(her_challenge,
                    localNode.cookie());
            sendChallengeAck(our_digest);
            connected = true;
            cookieOk = true;
            sendCookie = false;
        } catch (final IOException ie) {
            close();
            throw ie;
        } catch (final OtpAuthException ae) {
            close();
            throw ae;
        } catch (final Exception e) {
            final String nn = peer.node();
            close();
            final IOException ioe = new IOException(
                    "Error accepting connection from " + nn);
            ioe.initCause(e);
            throw ioe;
        }
        if (traceLevel >= handshakeThreshold) {
            System.out.println("<- MD5 ACCEPTED " + peer.host());
        }
    }

    protected void doConnect(final int port) throws IOException,
            OtpAuthException {
        try {
            socket = peer.createTransport(peer.host(), port);

            if (traceLevel >= handshakeThreshold) {
                System.out.println("-> MD5 CONNECT TO " + peer.host() + ":"
                        + port);
            }
            final int send_name_tag = sendName(peer.distChoose, localNode.flags,
                                               localNode.creation);
            recvStatus();
            final int her_challenge = recvChallenge();
            final byte[] our_digest = genDigest(her_challenge,
                    localNode.cookie());
            final int our_challenge = genChallenge();
            sendComplement(send_name_tag);
            sendChallengeReply(our_challenge, our_digest);
            recvChallengeAck(our_challenge);
            cookieOk = true;
            sendCookie = false;
        } catch (final OtpAuthException ae) {
            close();
            throw ae;
        } catch (final Exception e) {
            close();
            final IOException ioe = new IOException(
                    "Cannot connect to peer node");
            ioe.initCause(e);
            throw ioe;
        }
    }

    // This is nooo good as a challenge,
    // XXX fix me.
    static protected int genChallenge() {
        return random.nextInt();
    }

    // Used to debug print a message digest
    static String hex0(final byte x) {
        final char tab[] = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
                'a', 'b', 'c', 'd', 'e', 'f' };
        int uint;
        if (x < 0) {
            uint = x & 0x7F;
            uint |= 1 << 7;
        } else {
            uint = x;
        }
        return "" + tab[uint >>> 4] + tab[uint & 0xF];
    }

    static String hex(final byte[] b) {
        final StringBuffer sb = new StringBuffer();
        try {
            int i;
            for (i = 0; i < b.length; ++i) {
                sb.append(hex0(b[i]));
            }
        } catch (final Exception e) {
            // Debug function, ignore errors.
        }
        return sb.toString();

    }

    protected byte[] genDigest(final int challenge, final String cookie) {
        int i;
        long ch2;

        if (challenge < 0) {
            ch2 = 1L << 31;
            ch2 |= challenge & 0x7FFFFFFF;
        } else {
            ch2 = challenge;
        }
        final OtpMD5 context = new OtpMD5();
        context.update(cookie);
        context.update("" + ch2);

        final int[] tmp = context.final_bytes();
        final byte[] res = new byte[tmp.length];
        for (i = 0; i < tmp.length; ++i) {
            res[i] = (byte) (tmp[i] & 0xFF);
        }
        return res;
    }

    protected int sendName(final int dist, final long aflags,
                            final int creation)
            throws IOException {

        @SuppressWarnings("resource")
        final OtpOutputStream obuf = new OtpOutputStream();
        final String str = localNode.node();
        int send_name_tag;
        if (dist == 5) {
            obuf.write2BE(1+2+4 + str.length());
            send_name_tag = 'n';
            obuf.write1(send_name_tag);
            obuf.write2BE(dist);
            obuf.write4BE(aflags);
            obuf.write(str.getBytes());
        }
        else {
            obuf.write2BE(1+8+4+2 + str.length());
            send_name_tag = 'N';
            obuf.write1(send_name_tag);
            obuf.write8BE(aflags);
            obuf.write4BE(creation);
            obuf.write2BE(str.length());
            obuf.write(str.getBytes());
        }

        obuf.writeToAndFlush(socket.getOutputStream());

        if (traceLevel >= handshakeThreshold) {
            System.out.println("-> " + "HANDSHAKE sendName" + " flags="
                    + aflags + " dist=" + dist + " local=" + localNode);
        }
        return send_name_tag;
    }

    protected void sendComplement(final int send_name_tag)
            throws IOException {

        if (send_name_tag == 'n' &&
            (peer.flags & AbstractNode.dFlagHandshake23) != 0) {
            @SuppressWarnings("resource")
            final OtpOutputStream obuf = new OtpOutputStream();
            obuf.write2BE(1+4+4);
            obuf.write1('c');
            final int flagsHigh = (int)(localNode.flags >> 32);
            obuf.write4BE(flagsHigh);
            obuf.write4BE(localNode.creation);

            obuf.writeToAndFlush(socket.getOutputStream());

            if (traceLevel >= handshakeThreshold) {
                System.out.println("-> " + "HANDSHAKE sendComplement" +
                                   " flagsHigh=" + flagsHigh +
                                   " creation=" + localNode.creation);
            }
        }
    }

    protected void sendChallenge(final long her_flags, final long our_flags,
                                 final int challenge) throws IOException {

        @SuppressWarnings("resource")
        final OtpOutputStream obuf = new OtpOutputStream();
        final String str = localNode.node();
        if ((her_flags & AbstractNode.dFlagHandshake23) == 0) {
            obuf.write2BE(1+2+4+4 + str.length());
            obuf.write1('n');
            obuf.write2BE(5);
            obuf.write4BE(our_flags & 0xffffffff);
            obuf.write4BE(challenge);
            obuf.write(str.getBytes());
        }
        else {
            obuf.write2BE(1+8+4+4+2 + str.length());
            obuf.write1('N');
            obuf.write8BE(our_flags);
            obuf.write4BE(challenge);
            obuf.write4BE(localNode.creation);
            obuf.write2BE(str.length());
            obuf.write(str.getBytes());
        }

        obuf.writeToAndFlush(socket.getOutputStream());

        if (traceLevel >= handshakeThreshold) {
            System.out.println("-> " + "HANDSHAKE sendChallenge" + " flags="
                    + our_flags + " challenge=" + challenge
                    + " local=" + localNode);
        }
    }

    protected byte[] read2BytePackage() throws IOException,
            OtpErlangDecodeException {

        final byte[] lbuf = new byte[2];
        byte[] tmpbuf;

        readSock(socket, lbuf);
        @SuppressWarnings("resource")
        final OtpInputStream ibuf = new OtpInputStream(lbuf, 0);
        final int len = ibuf.read2BE();
        tmpbuf = new byte[len];
        readSock(socket, tmpbuf);
        return tmpbuf;
    }

    protected int recvName(final OtpPeer apeer) throws IOException {
        int send_name_tag;
        String hisname = "";

        try {
            final byte[] tmpbuf = read2BytePackage();
            @SuppressWarnings("resource")
            final OtpInputStream ibuf = new OtpInputStream(tmpbuf, 0);
            byte[] tmpname;
            final int len = tmpbuf.length;
            send_name_tag = ibuf.read1();
            switch (send_name_tag) {
            case 'n':
                apeer.distLow = apeer.distHigh = ibuf.read2BE();
                if (apeer.distLow != 5)
                    throw new IOException("Invalid handshake version");
                apeer.flags = ibuf.read4BE();
                tmpname = new byte[len - 7];
                ibuf.readN(tmpname);
                hisname = OtpErlangString.newString(tmpname);
                break;
            case 'N':
                apeer.distLow = apeer.distHigh = 6;
                apeer.flags = ibuf.read8BE();
                if ((apeer.flags & AbstractNode.dFlagHandshake23) == 0)
                    throw new IOException("Missing DFLAG_HANDSHAKE_23");
                apeer.creation = ibuf.read4BE();
                int namelen = ibuf.read2BE();
                tmpname = new byte[namelen];
                ibuf.readN(tmpname);
                hisname = OtpErlangString.newString(tmpname);
                break;
            default:
                throw new IOException("Unknown remote node type");
            }

            if ((apeer.flags & AbstractNode.dFlagExtendedReferences) == 0) {
                throw new IOException(
                        "Handshake failed - peer cannot handle extended references");
            }

            if ((apeer.flags & AbstractNode.dFlagExtendedPidsPorts) == 0) {
                throw new IOException(
                        "Handshake failed - peer cannot handle extended pids and ports");
            }

        } catch (final OtpErlangDecodeException e) {
            throw new IOException("Handshake failed - not enough data");
        }

        final int i = hisname.indexOf('@', 0);
        apeer.node = hisname;
        apeer.alive = hisname.substring(0, i);
        apeer.host = hisname.substring(i + 1, hisname.length());

        if (traceLevel >= handshakeThreshold) {
            System.out.println("<- " + "HANDSHAKE" + " ntype=" + apeer.ntype
                    + " dist=" + apeer.distHigh + " remote=" + apeer);
        }
        return send_name_tag;
    }

    protected int recvChallenge() throws IOException {

        int challenge;

        try {
            final byte[] buf = read2BytePackage();
            @SuppressWarnings("resource")
            final OtpInputStream ibuf = new OtpInputStream(buf, 0);
            int namelen;
            switch (ibuf.read1()) {
            case 'n':
                if (peer.distChoose != 5)
                    throw new IOException("Old challenge wrong version");
                peer.distLow = peer.distHigh = ibuf.read2BE();
                peer.flags = ibuf.read4BE();
                if ((peer.flags & AbstractNode.dFlagHandshake23) != 0)
                    throw new IOException("Old challenge unexpected DFLAG_HANDHAKE_23");
                challenge = ibuf.read4BE();
                namelen = buf.length - (1+2+4+4);
                break;
            case 'N':
                peer.distLow = peer.distHigh = peer.distChoose = 6;
                peer.flags = ibuf.read8BE();
                if ((peer.flags & AbstractNode.dFlagHandshake23) == 0)
                    throw new IOException("New challenge missing DFLAG_HANDHAKE_23");
                challenge = ibuf.read4BE();
                peer.creation = ibuf.read4BE();
                namelen = ibuf.read2BE();
                break;
            default:
                throw new IOException("Unexpected peer type");
            }
            final byte[] tmpname = new byte[namelen];
            ibuf.readN(tmpname);
            final String hisname = OtpErlangString.newString(tmpname);
            if (!hisname.equals(peer.node)) {
                throw new IOException(
                        "Handshake failed - peer has wrong name: " + hisname);
            }

            if ((peer.flags & AbstractNode.dFlagExtendedReferences) == 0) {
                throw new IOException(
                        "Handshake failed - peer cannot handle extended references");
            }

            if ((peer.flags & AbstractNode.dFlagExtendedPidsPorts) == 0) {
                throw new IOException(
                        "Handshake failed - peer cannot handle extended pids and ports");
            }

        } catch (final OtpErlangDecodeException e) {
            throw new IOException("Handshake failed - not enough data");
        }

        if (traceLevel >= handshakeThreshold) {
            System.out.println("<- " + "HANDSHAKE recvChallenge" + " from="
                    + peer.node + " challenge=" + challenge + " local="
                    + localNode);
        }

        return challenge;
    }

    protected void recvComplement(int send_name_tag) throws IOException {

        if (send_name_tag == 'n' &&
            (peer.flags & AbstractNode.dFlagHandshake23) != 0) {
            try {
                final byte[] tmpbuf = read2BytePackage();
                @SuppressWarnings("resource")
                final OtpInputStream ibuf = new OtpInputStream(tmpbuf, 0);
                if (ibuf.read1() != 'c')
                    throw new IOException("Not a complement tag");

                final long flagsHigh = ibuf.read4BE();
                peer.flags |= flagsHigh << 32;
                peer.creation = ibuf.read4BE();

            } catch (final OtpErlangDecodeException e) {
                throw new IOException("Handshake failed - not enough data");
            }
        }
    }

    protected void sendChallengeReply(final int challenge, final byte[] digest)
            throws IOException {

        @SuppressWarnings("resource")
        final OtpOutputStream obuf = new OtpOutputStream();
        obuf.write2BE(21);
        obuf.write1(ChallengeReply);
        obuf.write4BE(challenge);
        obuf.write(digest);
        obuf.writeToAndFlush(socket.getOutputStream());

        if (traceLevel >= handshakeThreshold) {
            System.out.println("-> " + "HANDSHAKE sendChallengeReply"
                    + " challenge=" + challenge + " digest=" + hex(digest)
                    + " local=" + localNode);
        }
    }

    // Would use Array.equals in newer JDK...
    private boolean digests_equals(final byte[] a, final byte[] b) {
        int i;
        for (i = 0; i < 16; ++i) {
            if (a[i] != b[i]) {
                return false;
            }
        }
        return true;
    }

    protected int recvChallengeReply(final int our_challenge)
            throws IOException, OtpAuthException {

        int challenge;
        final byte[] her_digest = new byte[16];

        try {
            final byte[] buf = read2BytePackage();
            @SuppressWarnings("resource")
            final OtpInputStream ibuf = new OtpInputStream(buf, 0);
            final int tag = ibuf.read1();
            if (tag != ChallengeReply) {
                throw new IOException("Handshake protocol error");
            }
            challenge = ibuf.read4BE();
            ibuf.readN(her_digest);
            final byte[] our_digest = genDigest(our_challenge,
                    localNode.cookie());
            if (!digests_equals(her_digest, our_digest)) {
                throw new OtpAuthException("Peer authentication error.");
            }
        } catch (final OtpErlangDecodeException e) {
            throw new IOException("Handshake failed - not enough data");
        }

        if (traceLevel >= handshakeThreshold) {
            System.out.println("<- " + "HANDSHAKE recvChallengeReply"
                    + " from=" + peer.node + " challenge=" + challenge
                    + " digest=" + hex(her_digest) + " local=" + localNode);
        }

        return challenge;
    }

    protected void sendChallengeAck(final byte[] digest) throws IOException {

        @SuppressWarnings("resource")
        final OtpOutputStream obuf = new OtpOutputStream();
        obuf.write2BE(17);
        obuf.write1(ChallengeAck);
        obuf.write(digest);

        obuf.writeToAndFlush(socket.getOutputStream());

        if (traceLevel >= handshakeThreshold) {
            System.out.println("-> " + "HANDSHAKE sendChallengeAck"
                    + " digest=" + hex(digest) + " local=" + localNode);
        }
    }

    protected void recvChallengeAck(final int our_challenge)
            throws IOException, OtpAuthException {

        final byte[] her_digest = new byte[16];
        try {
            final byte[] buf = read2BytePackage();
            @SuppressWarnings("resource")
            final OtpInputStream ibuf = new OtpInputStream(buf, 0);
            final int tag = ibuf.read1();
            if (tag != ChallengeAck) {
                throw new IOException("Handshake protocol error");
            }
            ibuf.readN(her_digest);
            final byte[] our_digest = genDigest(our_challenge,
                    localNode.cookie());
            if (!digests_equals(her_digest, our_digest)) {
                throw new OtpAuthException("Peer authentication error.");
            }
        } catch (final OtpErlangDecodeException e) {
            throw new IOException("Handshake failed - not enough data");
        } catch (final Exception e) {
            throw new OtpAuthException("Peer authentication error.");
        }

        if (traceLevel >= handshakeThreshold) {
            System.out.println("<- " + "HANDSHAKE recvChallengeAck" + " from="
                    + peer.node + " digest=" + hex(her_digest) + " local="
                    + localNode);
        }
    }

    protected void sendStatus(final String status) throws IOException {

        @SuppressWarnings("resource")
        final OtpOutputStream obuf = new OtpOutputStream();
        obuf.write2BE(status.length() + 1);
        obuf.write1(ChallengeStatus);
        obuf.write(status.getBytes());

        obuf.writeToAndFlush(socket.getOutputStream());

        if (traceLevel >= handshakeThreshold) {
            System.out.println("-> " + "HANDSHAKE sendStatus" + " status="
                    + status + " local=" + localNode);
        }
    }

    protected void recvStatus() throws IOException {

        try {
            final byte[] buf = read2BytePackage();
            @SuppressWarnings("resource")
            final OtpInputStream ibuf = new OtpInputStream(buf, 0);
            final int tag = ibuf.read1();
            if (tag != ChallengeStatus) {
                throw new IOException("Handshake protocol error");
            }
            final byte[] tmpbuf = new byte[buf.length - 1];
            ibuf.readN(tmpbuf);
            final String status = OtpErlangString.newString(tmpbuf);

            if (status.compareTo("ok") != 0) {
                throw new IOException("Peer replied with status '" + status
                        + "' instead of 'ok'");
            }
        } catch (final OtpErlangDecodeException e) {
            throw new IOException("Handshake failed - not enough data");
        }
        if (traceLevel >= handshakeThreshold) {
            System.out.println("<- " + "HANDSHAKE recvStatus (ok)" + " local="
                    + localNode);
        }
    }

    public void setFlags(final int flags) {
        this.flags = flags;
    }

    public int getFlags() {
        return flags;
    }
}
