/*
 * Copyright 2011-17 Fraunhofer ISE, energy & meteo Systems GmbH and other contributors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */
package org.openmuc.josistack;

import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.net.InetAddress;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.TimeoutException;

import org.openmuc.jasn1.ber.BerByteArrayOutputStream;
import org.openmuc.jasn1.ber.types.BerAnyNoDecode;
import org.openmuc.jasn1.ber.types.BerInteger;
import org.openmuc.jasn1.ber.types.BerObjectIdentifier;
import org.openmuc.jasn1.ber.types.string.BerGraphicString;
import org.openmuc.josistack.internal.acse.asn1.AAREApdu;
import org.openmuc.josistack.internal.acse.asn1.AARQApdu;
import org.openmuc.josistack.internal.acse.asn1.ACSEApdu;
import org.openmuc.josistack.internal.acse.asn1.ACSERequirements;
import org.openmuc.josistack.internal.acse.asn1.AEQualifier;
import org.openmuc.josistack.internal.acse.asn1.AEQualifierForm2;
import org.openmuc.josistack.internal.acse.asn1.APTitle;
import org.openmuc.josistack.internal.acse.asn1.APTitleForm2;
import org.openmuc.josistack.internal.acse.asn1.AssociateResult;
import org.openmuc.josistack.internal.acse.asn1.AssociateSourceDiagnostic;
import org.openmuc.josistack.internal.acse.asn1.AssociationInformation;
import org.openmuc.josistack.internal.acse.asn1.AuthenticationValue;
import org.openmuc.josistack.internal.acse.asn1.MechanismName;
import org.openmuc.josistack.internal.acse.asn1.Myexternal;
import org.openmuc.josistack.internal.presentation.asn1.CPAPPDU;
import org.openmuc.josistack.internal.presentation.asn1.CPType;
import org.openmuc.josistack.internal.presentation.asn1.CalledPresentationSelector;
import org.openmuc.josistack.internal.presentation.asn1.CallingPresentationSelector;
import org.openmuc.josistack.internal.presentation.asn1.FullyEncodedData;
import org.openmuc.josistack.internal.presentation.asn1.ModeSelector;
import org.openmuc.josistack.internal.presentation.asn1.PDVList;
import org.openmuc.josistack.internal.presentation.asn1.PresentationContextDefinitionList;
import org.openmuc.josistack.internal.presentation.asn1.PresentationContextDefinitionResultList;
import org.openmuc.josistack.internal.presentation.asn1.PresentationContextIdentifier;
import org.openmuc.josistack.internal.presentation.asn1.RespondingPresentationSelector;
import org.openmuc.josistack.internal.presentation.asn1.UserData;
import org.openmuc.jositransport.ClientTSap;
import org.openmuc.jositransport.TConnection;

public final class AcseAssociation {

    // private static final Logger logger = LoggerFactory.getLogger(AcseAssociation.class);

    private boolean connected = false;
    private TConnection tConnection;
    private ByteBuffer associateResponseAPDU = null;
    private Long currentCallingSessionSelector = null;
    private final RespondingPresentationSelector pSelLocalBerOctetString;

    private static final PresentationContextDefinitionList context_list = new PresentationContextDefinitionList(
            new byte[] { (byte) 0x23, (byte) 0x30, (byte) 0x0f, (byte) 0x02, (byte) 0x01, (byte) 0x01, (byte) 0x06,
                    (byte) 0x04, (byte) 0x52, (byte) 0x01, (byte) 0x00, (byte) 0x01, (byte) 0x30, (byte) 0x04,
                    (byte) 0x06, (byte) 0x02, (byte) 0x51, (byte) 0x01, (byte) 0x30, (byte) 0x10, (byte) 0x02,
                    (byte) 0x01, (byte) 0x03, (byte) 0x06, (byte) 0x05, (byte) 0x28, (byte) 0xca, (byte) 0x22,
                    (byte) 0x02, (byte) 0x01, (byte) 0x30, (byte) 0x04, (byte) 0x06, (byte) 0x02, (byte) 0x51,
                    (byte) 0x01 });

    private static final PresentationContextIdentifier acsePresentationContextId = new PresentationContextIdentifier(
            new byte[] { (byte) 0x01, (byte) 0x01 });
    private static final ModeSelector normalModeSelector = new ModeSelector(new BerInteger(1));

    private static final PresentationContextDefinitionResultList presentationResultList = new PresentationContextDefinitionResultList(
            new byte[] { (byte) 0x12, (byte) 0x30, (byte) 0x07, (byte) 0x80, (byte) 0x01, (byte) 0x00, (byte) 0x81,
                    (byte) 0x02, (byte) 0x51, (byte) 0x01, (byte) 0x30, (byte) 0x07, (byte) 0x80, (byte) 0x01,
                    (byte) 0x00, (byte) 0x81, (byte) 0x02, (byte) 0x51, (byte) 0x01 });

    private static final AssociateResult aareAccepted = new AssociateResult(new byte[] { (byte) 0x01, (byte) 0x00 });

    private static final AssociateSourceDiagnostic associateSourceDiagnostic = new AssociateSourceDiagnostic(
            new byte[] { (byte) 0xa1, (byte) 0x03, (byte) 0x02, (byte) 0x01, (byte) 0x00 });

    // is always equal to 1.0.9506.2.3 (MMS)
    private static final BerObjectIdentifier application_context_name = new BerObjectIdentifier(
            new byte[] { (byte) 0x05, (byte) 0x28, (byte) 0xca, (byte) 0x22, (byte) 0x02, (byte) 0x03 });

    private static final BerObjectIdentifier directReference = new BerObjectIdentifier(
            new byte[] { (byte) 0x02, (byte) 0x51, (byte) 0x01 });
    private static final BerInteger indirectReference = new BerInteger(new byte[] { (byte) 0x01, (byte) 0x03 });

    private static final MechanismName default_mechanism_name = new MechanismName(
            new byte[] { 0x03, 0x52, 0x03, 0x01 });

    AcseAssociation(TConnection tConnection, byte[] pSelLocal) {
        this.tConnection = tConnection;
        pSelLocalBerOctetString = new RespondingPresentationSelector(pSelLocal);
    }

    /**
     * A server that got an Association Request Indication may use this function to accept the association.
     * 
     * @param payload
     *            the payload to send with the accept message
     * @throws IOException
     *             if an error occures accepting the association
     */
    public void accept(ByteBuffer payload) throws IOException {

        int payloadLength = payload.limit() - payload.position();

        Myexternal.Encoding encoding = new Myexternal.Encoding(new BerAnyNoDecode(payloadLength), null, null);

        Myexternal myExternal = new Myexternal(directReference, indirectReference, encoding);

        List<Myexternal> externalList = new ArrayList<>(1);
        externalList.add(myExternal);

        AssociationInformation userInformation = new AssociationInformation(externalList);

        AAREApdu aare = new AAREApdu(null, application_context_name, aareAccepted, associateSourceDiagnostic, null,
                null, null, null, null, null, null, null, null, userInformation);

        ACSEApdu acse = new ACSEApdu(null, aare, null, null);

        BerByteArrayOutputStream berOStream = new BerByteArrayOutputStream(100, true);
        acse.encode(berOStream, true);
        int acseHeaderLength = berOStream.buffer.length - (berOStream.index + 1);

        UserData userData = getPresentationUserDataField(acseHeaderLength + payloadLength);
        CPAPPDU.NormalModeParameters normalModeParameters = new CPAPPDU.NormalModeParameters(null,
                pSelLocalBerOctetString, presentationResultList, null, null, userData);

        CPAPPDU cpaPPdu = new CPAPPDU(normalModeSelector, normalModeParameters);

        cpaPPdu.encode(berOStream, true);

        List<byte[]> ssduList = new LinkedList<>();
        List<Integer> ssduOffsets = new LinkedList<>();
        List<Integer> ssduLengths = new LinkedList<>();

        ssduList.add(berOStream.buffer);
        ssduOffsets.add(berOStream.index + 1);
        ssduLengths.add(berOStream.buffer.length - (berOStream.index + 1));

        ssduList.add(payload.array());
        ssduOffsets.add(payload.arrayOffset() + payload.position());
        ssduLengths.add(payloadLength);

        writeSessionAccept(ssduList, ssduOffsets, ssduLengths);

        connected = true;
    }

    private void writeSessionAccept(List<byte[]> ssdu, List<Integer> ssduOffsets, List<Integer> ssduLengths)
            throws IOException {
        byte[] sduAcceptHeader = new byte[20];
        int idx = 0;

        int ssduLength = 0;
        for (int ssduElementLength : ssduLengths) {
            ssduLength += ssduElementLength;
        }

        // write ISO 8327-1 Header
        // SPDU Type: ACCEPT (14)
        sduAcceptHeader[idx++] = 0x0e;
        // Length: length of session user data + 22 ( header data after length
        // field )
        sduAcceptHeader[idx++] = (byte) ((ssduLength + 18) & 0xff);

        // -- start Connect Accept Item
        // Parameter type: Connect Accept Item (5)
        sduAcceptHeader[idx++] = 0x05;
        // Parameter length
        sduAcceptHeader[idx++] = 0x06;

        // Protocol options:
        // Parameter Type: Protocol Options (19)
        sduAcceptHeader[idx++] = 0x13;
        // Parameter length
        sduAcceptHeader[idx++] = 0x01;
        // flags: (.... ...0 = Able to receive extended concatenated SPDU:
        // False)
        sduAcceptHeader[idx++] = 0x00;

        // Version number:
        // Parameter type: Version Number (22)
        sduAcceptHeader[idx++] = 0x16;
        // Parameter length
        sduAcceptHeader[idx++] = 0x01;
        // flags: (.... ..1. = Protocol Version 2: True)
        sduAcceptHeader[idx++] = 0x02;
        // -- end Connect Accept Item

        // Session Requirement
        // Parameter type: Session Requirement (20)
        sduAcceptHeader[idx++] = 0x14;
        // Parameter length
        sduAcceptHeader[idx++] = 0x02;
        // flags: (.... .... .... ..1. = Duplex functional unit: True)
        sduAcceptHeader[idx++] = 0x00;
        sduAcceptHeader[idx++] = 0x02;

        // Called Session Selector
        // Parameter type: Called Session Selector (52)
        sduAcceptHeader[idx++] = 0x34;
        // Parameter length
        sduAcceptHeader[idx++] = 0x02;
        // Called Session Selector
        sduAcceptHeader[idx++] = 0x00;
        sduAcceptHeader[idx++] = 0x01;

        // Session user data
        // Parameter type: Session user data (193)
        sduAcceptHeader[idx++] = (byte) 0xc1;

        // Parameter length
        sduAcceptHeader[idx++] = (byte) ssduLength;

        ssdu.add(0, sduAcceptHeader);
        ssduOffsets.add(0, 0);
        ssduLengths.add(0, sduAcceptHeader.length);

        tConnection.send(ssdu, ssduOffsets, ssduLengths);

    }

    public ByteBuffer getAssociateResponseAPdu() {
        ByteBuffer returnBuffer = associateResponseAPDU;
        associateResponseAPDU = null;
        return returnBuffer;
    }

    /**
     * Starts an Application Association by sending an association request and waiting for an association accept message
     * 
     * @param payload
     *            payload that can be sent with the association request
     * @param port
     * @param address
     * @param tSAP
     * @param aeQualifierCalling
     * @param aeQualifierCalled
     * @param apTitleCalling
     * @param apTitleCalled
     * @throws IOException
     */
    void startAssociation(ByteBuffer payload, InetAddress address, int port, InetAddress localAddr, int localPort,
            String authenticationParameter, byte[] sSelRemote, byte[] sSelLocal, byte[] pSelRemote, ClientTSap tSAP,
            int[] apTitleCalled, int[] apTitleCalling, int aeQualifierCalled, int aeQualifierCalling)
            throws IOException {
        if (connected == true) {
            throw new IOException();
        }

        int payloadLength = payload.limit() - payload.position();

        APTitle called_ap_title = new APTitle(new APTitleForm2(apTitleCalled));
        APTitle calling_ap_title = new APTitle(new APTitleForm2(apTitleCalling));

        AEQualifier called_ae_qualifier = new AEQualifier(new AEQualifierForm2(aeQualifierCalled));
        AEQualifier calling_ae_qualifier = new AEQualifier(new AEQualifierForm2(aeQualifierCalling));

        Myexternal.Encoding encoding = new Myexternal.Encoding(new BerAnyNoDecode(payloadLength), null, null);

        Myexternal myExternal = new Myexternal(directReference, indirectReference, encoding);

        List<Myexternal> externalList = new ArrayList<>(1);
        externalList.add(myExternal);

        AssociationInformation userInformation = new AssociationInformation(externalList);

        ACSERequirements sender_acse_requirements = null;
        MechanismName mechanism_name = null;
        AuthenticationValue authentication_value = null;
        if (authenticationParameter != null) {
            sender_acse_requirements = new ACSERequirements(new byte[] { (byte) 0x02, (byte) 0x07, (byte) 0x80 });
            mechanism_name = default_mechanism_name;
            authentication_value = new AuthenticationValue(new BerGraphicString(authenticationParameter.getBytes()),
                    null, null);
        }

        AARQApdu aarq = new AARQApdu(null, application_context_name, called_ap_title, called_ae_qualifier, null, null,
                calling_ap_title, calling_ae_qualifier, null, null, sender_acse_requirements, mechanism_name,
                authentication_value, null, null, userInformation);
        ACSEApdu acse = new ACSEApdu(aarq, null, null, null);

        BerByteArrayOutputStream berOStream = new BerByteArrayOutputStream(200, true);
        acse.encode(berOStream, true);
        int acseHeaderLength = berOStream.buffer.length - (berOStream.index + 1);

        UserData userData = getPresentationUserDataField(acseHeaderLength + payloadLength);

        CPType.NormalModeParameters normalModeParameter = new CPType.NormalModeParameters(null,
                new CallingPresentationSelector(pSelLocalBerOctetString.value),
                new CalledPresentationSelector(pSelRemote), context_list, null, null, null, userData);

        CPType cpType = new CPType(normalModeSelector, normalModeParameter);

        cpType.encode(berOStream, true);

        List<byte[]> ssduList = new LinkedList<>();
        List<Integer> ssduOffsets = new LinkedList<>();
        List<Integer> ssduLengths = new LinkedList<>();

        ssduList.add(berOStream.buffer);
        ssduOffsets.add(berOStream.index + 1);
        ssduLengths.add(berOStream.buffer.length - (berOStream.index + 1));

        ssduList.add(payload.array());
        ssduOffsets.add(payload.arrayOffset() + payload.position());
        ssduLengths.add(payloadLength);

        ByteBuffer res = null;
        res = startSConnection(ssduList, ssduOffsets, ssduLengths, address, port, localAddr, localPort, tSAP,
                sSelRemote, sSelLocal);

        associateResponseAPDU = decodePConResponse(res);

    }

    private static ByteBuffer decodePConResponse(ByteBuffer ppdu) throws IOException {

        CPAPPDU cpa_ppdu = new CPAPPDU();
        InputStream iStream = new ByteBufferInputStream(ppdu);
        cpa_ppdu.decode(iStream, true);

        ACSEApdu acseApdu = new ACSEApdu();
        acseApdu.decode(iStream, null);

        return ppdu;

    }

    private static UserData getPresentationUserDataField(int userDataLength) {
        PDVList.PresentationDataValues presDataValues = new PDVList.PresentationDataValues(
                new BerAnyNoDecode(userDataLength), null, null);
        PDVList pdvList = new PDVList(null, acsePresentationContextId, presDataValues);
        List<PDVList> pdvListList = new ArrayList<>(1);
        pdvListList.add(pdvList);

        FullyEncodedData fullyEncodedData = new FullyEncodedData(pdvListList);

        UserData userData = new UserData(null, fullyEncodedData);
        return userData;
    }

    /**
     * Starts a session layer connection, sends a CONNECT (CN), waits for a ACCEPT (AC) and throws an IOException if not
     * successful
     * 
     * @throws IOException
     */
    private ByteBuffer startSConnection(List<byte[]> ssduList, List<Integer> ssduOffsets, List<Integer> ssduLengths,
            InetAddress address, int port, InetAddress localAddr, int localPort, ClientTSap tSAP, byte[] sSelRemote,
            byte[] sSelLocal) throws IOException {
        if (connected == true) {
            throw new IOException();
        }

        byte[] spduHeader = new byte[24];
        int idx = 0;
        // byte[] res = null;

        int ssduLength = 0;
        for (int ssduElementLength : ssduLengths) {
            ssduLength += ssduElementLength;
        }

        // write ISO 8327-1 Header
        // SPDU Type: CONNECT (13)
        spduHeader[idx++] = 0x0d;
        // Length: length of session user data + 22 ( header data after
        // length field )
        spduHeader[idx++] = (byte) ((ssduLength + 22) & 0xff);

        // -- start Connect Accept Item
        // Parameter type: Connect Accept Item (5)
        spduHeader[idx++] = 0x05;
        // Parameter length
        spduHeader[idx++] = 0x06;

        // Protocol options:
        // Parameter Type: Protocol Options (19)
        spduHeader[idx++] = 0x13;
        // Parameter length
        spduHeader[idx++] = 0x01;
        // flags: (.... ...0 = Able to receive extended concatenated SPDU:
        // False)
        spduHeader[idx++] = 0x00;

        // Version number:
        // Parameter type: Version Number (22)
        spduHeader[idx++] = 0x16;
        // Parameter length
        spduHeader[idx++] = 0x01;
        // flags: (.... ..1. = Protocol Version 2: True)
        spduHeader[idx++] = 0x02;
        // -- end Connect Accept Item

        // Session Requirement
        // Parameter type: Session Requirement (20)
        spduHeader[idx++] = 0x14;
        // Parameter length
        spduHeader[idx++] = 0x02;
        // flags: (.... .... .... ..1. = Duplex functional unit: True)
        spduHeader[idx++] = 0x00;
        spduHeader[idx++] = 0x02;

        // Calling Session Selector
        // Parameter type: Calling Session Selector (51)
        spduHeader[idx++] = 0x33;
        // Parameter length
        spduHeader[idx++] = 0x02;
        // Calling Session Selector
        spduHeader[idx++] = sSelRemote[0];
        spduHeader[idx++] = sSelRemote[1];

        // Called Session Selector
        // Parameter type: Called Session Selector (52)
        spduHeader[idx++] = 0x34;
        // Parameter length
        spduHeader[idx++] = 0x02;
        // Called Session Selector
        spduHeader[idx++] = sSelLocal[0];
        spduHeader[idx++] = sSelLocal[1];

        // Session user data
        // Parameter type: Session user data (193)
        spduHeader[idx++] = (byte) 0xc1;
        // Parameter length
        spduHeader[idx++] = (byte) (ssduLength & 0xff);
        // write session user data

        ssduList.add(0, spduHeader);
        ssduOffsets.add(0, 0);
        ssduLengths.add(0, spduHeader.length);

        tConnection = tSAP.connectTo(address, port, localAddr, localPort);

        tConnection.send(ssduList, ssduOffsets, ssduLengths);

        // TODO how much should be allocated here?
        ByteBuffer pduBuffer = ByteBuffer.allocate(500);

        try {
            tConnection.receive(pduBuffer);
        } catch (TimeoutException e) {
            throw new IOException("ResponseTimeout waiting for connection response.", e);
        }
        idx = 0;

        // read ISO 8327-1 Header
        // SPDU Type: ACCEPT (14)
        byte spduType = pduBuffer.get();
        if (spduType != 0x0e) {
            throw new IOException("ISO 8327-1 header wrong SPDU type, expected ACCEPT (14), got "
                    + getSPDUTypeString(spduType) + " (" + spduType + ")");
        }
        pduBuffer.get(); // skip length byte

        parameter_loop: while (true) {
            // read parameter type
            int parameterType = pduBuffer.get() & 0xff;
            // read parameter length
            int parameterLength = pduBuffer.get() & 0xff;

            switch (parameterType) {
            // Connect Accept Item (5)
            case 0x05:
                int bytesToRead = parameterLength;
                while (bytesToRead > 0) {
                    // read parameter type
                    int ca_parameterType = pduBuffer.get();
                    // read parameter length
                    // int ca_parameterLength = res[idx++];
                    pduBuffer.get();

                    bytesToRead -= 2;

                    switch (ca_parameterType & 0xff) {
                    // Protocol Options (19)
                    case 0x13:
                        // flags: .... ...0 = Able to receive extended
                        // concatenated SPDU: False
                        byte protocolOptions = pduBuffer.get();
                        if (protocolOptions != 0x00) {
                            throw new IOException(
                                    "SPDU Connect Accept Item/Protocol Options is " + protocolOptions + ", expected 0");
                        }

                        bytesToRead--;
                        break;
                    // Version Number
                    case 0x16:
                        // flags .... ..1. = Protocol Version 2: True
                        byte versionNumber = pduBuffer.get();
                        if (versionNumber != 0x02) {
                            throw new IOException(
                                    "SPDU Connect Accept Item/Version Number is " + versionNumber + ", expected 2");
                        }

                        bytesToRead--;
                        break;
                    default:
                        throw new IOException(
                                "SPDU Connect Accept Item: parameter not implemented: " + ca_parameterType);
                    }
                }
                break;
            // Session Requirement (20)
            case 0x14:
                // flags: (.... .... .... ..1. = Duplex functional unit: True)
                long sessionRequirement = extractInteger(pduBuffer, parameterLength);
                if (sessionRequirement != 0x02) {
                    throw new IOException("SPDU header parameter 'Session Requirement (20)' is " + sessionRequirement
                            + ", expected 2");

                }
                break;
            // Calling Session Selector (51)
            case 0x33:
                long css = extractInteger(pduBuffer, parameterLength);
                if (css != 0x01) {
                    throw new IOException(
                            "SPDU header parameter 'Calling Session Selector (51)' is " + css + ", expected 1");

                }
                break;
            // Called Session Selector (52)
            case 0x34:
                long calledSessionSelector = extractInteger(pduBuffer, parameterLength);
                if (calledSessionSelector != 0x01) {
                    throw new IOException("SPDU header parameter 'Called Session Selector (52)' is "
                            + calledSessionSelector + ", expected 1");
                }
                break;
            // Session user data (193)
            case 0xc1:
                break parameter_loop;
            default:
                throw new IOException("SPDU header parameter type " + parameterType + " not implemented");
            }
        }

        // got correct ACCEPT (AC) from the server

        connected = true;

        return pduBuffer;
    }

    public void send(ByteBuffer payload) throws IOException {

        List<byte[]> ssduList = new ArrayList<>();
        List<Integer> ssduOffsets = new LinkedList<>();
        List<Integer> ssduLengths = new LinkedList<>();

        encodePresentationLayer(payload, ssduList, ssduOffsets, ssduLengths);

        encodeSessionLayer(ssduList, ssduOffsets, ssduLengths);

        tConnection.send(ssduList, ssduOffsets, ssduLengths);
    }

    private void encodePresentationLayer(ByteBuffer payload, List<byte[]> ssduList, List<Integer> ssduOffsets,
            List<Integer> ssduLengths) throws IOException {
        PDVList pdv_list = new PDVList(null, new PresentationContextIdentifier(3l), new PDVList.PresentationDataValues(
                new BerAnyNoDecode(payload.limit() - payload.position()), null, null));
        List<PDVList> pdv_list_list = new ArrayList<>();
        pdv_list_list.add(pdv_list);
        FullyEncodedData fully_encoded_data = new FullyEncodedData(pdv_list_list);
        UserData user_data = new UserData(null, fully_encoded_data);

        BerByteArrayOutputStream berOStream = new BerByteArrayOutputStream(200, true);
        user_data.encode(berOStream, true);

        ssduList.add(berOStream.buffer);
        ssduOffsets.add(berOStream.index + 1);
        ssduLengths.add(berOStream.buffer.length - (berOStream.index + 1));

        ssduList.add(payload.array());
        ssduOffsets.add(payload.arrayOffset() + payload.position());
        ssduLengths.add(payload.limit() - payload.position());
    }

    private void encodeSessionLayer(List<byte[]> ssduList, List<Integer> ssduOffsets, List<Integer> ssduLengths)
            throws IOException {

        byte[] spduHeader = new byte[4];
        // --write iso 8327-1 Header--
        // write SPDU Type: give tokens PDU
        spduHeader[0] = 0x01;
        // length 0
        spduHeader[1] = 0;
        // write SPDU Type: DATA TRANSFER (DT)
        spduHeader[2] = 0x01;
        // length 0
        spduHeader[3] = 0;

        ssduList.add(0, spduHeader);
        ssduOffsets.add(0, 0);
        ssduLengths.add(0, spduHeader.length);

    }

    /**
     * Listens for a new PDU and writes it into the given buffer. Decodes all ACSE and lower layer headers. The
     * resulting buffer's position points to the beginning of the ACSE SDU. The limit will point to the byte after the
     * last byte of the ACSE SDU.
     * 
     * @param pduBuffer
     *            buffer to write the received pdu into
     * @throws DecodingException
     *             if a decoding error occurs
     * @throws IOException
     *             if a non recoverable error occurs. Afterwards the association should be closed by the user
     * @throws TimeoutException
     *             if a timeout occurs
     */
    public void receive(ByteBuffer pduBuffer) throws DecodingException, IOException, TimeoutException {
        if (connected == false) {
            throw new IllegalStateException("ACSE Association not connected");
        }
        tConnection.receive(pduBuffer);

        decodeSessionLayer(pduBuffer);

        decodePresentationLayer(pduBuffer);
    }

    private void decodePresentationLayer(ByteBuffer pduBuffer) throws DecodingException {
        // decode PPDU header
        UserData user_data = new UserData();

        try {
            user_data.decode(new ByteBufferInputStream(pduBuffer), null);
        } catch (IOException e) {
            throw new DecodingException("error decoding PPDU header", e);
        }
    }

    private void decodeSessionLayer(ByteBuffer pduBuffer) throws EOFException, DecodingException {
        int firstByte = pduBuffer.get();

        if (firstByte == 25) {
            // got an ABORT SPDU
            throw new EOFException("Received an ABORT SPDU");
        }

        // -- read ISO 8327-1 header
        // SPDU type: Give tokens PDU (1)
        if (firstByte != 0x01) {
            throw new DecodingException("SPDU header syntax errror: first SPDU type not 1");
        }
        // length
        if (pduBuffer.get() != 0) {
            throw new DecodingException("SPDU header syntax errror: first SPDU type length not 0");
        }
        // SPDU Type: DATA TRANSFER (DT) SPDU (1)
        if (pduBuffer.get() != 0x01) {
            throw new DecodingException("SPDU header syntax errror: second SPDU type not 1");
        }
        // length
        if (pduBuffer.get() != 0) {
            throw new DecodingException("SPDU header syntax errror: second SPDU type length not 0");
        }
    }

    /**
     * Disconnects by sending a disconnect request at the Transport Layer and then closing the socket.
     */
    public void disconnect() {
        connected = false;
        if (tConnection != null) {
            tConnection.disconnect();
        }
    }

    /**
     * Closes the connection simply by closing the socket.
     */
    public void close() {
        connected = false;
        if (tConnection != null) {
            tConnection.close();
        }
    }

    private long extractInteger(ByteBuffer buffer, int size) throws IOException {
        switch (size) {
        case 1:
            return buffer.get();
        case 2:
            return buffer.getShort();
        case 4:
            return buffer.getInt();
        case 8:
            return buffer.getLong();
        default:
            throw new IOException("invalid length for reading numeric value");
        }
    }

    void listenForCn(ByteBuffer pduBuffer) throws IOException, TimeoutException {
        if (connected == true) {
            throw new IllegalStateException("ACSE Association is already connected");
        }
        int parameter;
        int parameterLength;

        tConnection.receive(pduBuffer);
        // start reading ISO 8327-1 header
        // SPDU Type: CONNECT (CN) SPDU (13)
        byte spduType = pduBuffer.get();
        if (spduType != 0x0d) {
            throw new IOException("ISO 8327-1 header wrong SPDU type, expected CONNECT (13), got "
                    + getSPDUTypeString(spduType) + " (" + spduType + ")");
        }
        pduBuffer.get(); // skip lenght byte

        parameter_loop: while (true) {
            // read parameter code
            parameter = pduBuffer.get() & 0xff;
            // read parameter length
            parameterLength = pduBuffer.get() & 0xff;
            switch (parameter) {
            // Connect Accept Item (5)
            case 0x05:
                int bytesToRead = parameterLength;
                while (bytesToRead > 0) {
                    // read parameter type
                    int ca_parameterType = pduBuffer.get();
                    // read parameter length
                    pduBuffer.get();

                    bytesToRead -= 2;

                    switch (ca_parameterType & 0xff) {
                    // Protocol Options (19)
                    case 0x13:
                        // flags: .... ...0 = Able to receive extended
                        // concatenated SPDU: False
                        byte protocolOptions = pduBuffer.get();
                        if (protocolOptions != 0x00) {
                            throw new IOException(
                                    "SPDU Connect Accept Item/Protocol Options is " + protocolOptions + ", expected 0");
                        }

                        bytesToRead--;
                        break;
                    // Version Number
                    case 0x16:
                        // flags .... ..1. = Protocol Version 2: True
                        byte versionNumber = pduBuffer.get();
                        if (versionNumber != 0x02) {
                            throw new IOException(
                                    "SPDU Connect Accept Item/Version Number is " + versionNumber + ", expected 2");
                        }

                        bytesToRead--;
                        break;
                    default:
                        throw new IOException(
                                "SPDU Connect Accept Item: parameter not implemented: " + ca_parameterType);
                    }
                }
                break;
            // Session Requirement (20)
            case 0x14:
                // flags: (.... .... .... ..1. = Duplex functional unit: True)
                long sessionRequirement = extractInteger(pduBuffer, parameterLength);
                if (sessionRequirement != 0x02) {
                    throw new IOException("SPDU header parameter 'Session Requirement (20)' is " + sessionRequirement
                            + ", expected 2");
                }
                break;
            // Calling Session Selector (51)
            case 0x33:
                currentCallingSessionSelector = extractInteger(pduBuffer, parameterLength);
                break;
            // Called Session Selector (52)
            case 0x34:
                long calledSessionSelector = extractInteger(pduBuffer, parameterLength);
                if (calledSessionSelector != 0x01) {
                    throw new IOException("SPDU header parameter 'Called Session Selector (52)' is "
                            + calledSessionSelector + ", expected 1");
                }
                break;
            // Session user data (193)
            case 0xc1:
                break parameter_loop;
            default:
                throw new IOException("SPDU header parameter type " + parameter + " not implemented");
            }

        }

        CPType cpType = new CPType();
        ByteBufferInputStream iStream = new ByteBufferInputStream(pduBuffer);
        cpType.decode(iStream, true);

        ACSEApdu acseApdu = new ACSEApdu();
        acseApdu.decode(iStream, null);
    }

    public int getMessageTimeout() {
        return tConnection.getMessageTimeout();
    }

    public void setMessageTimeout(int i) {
        tConnection.setMessageTimeout(i);
    }

    public static String getSPDUTypeString(byte spduType) {
        switch (spduType) {
        case 0:
            return "EXCEPTION REPORT (ER)";
        case 1:
            return "DATA TRANSFER (DT)";
        case 2:
            return "PLEASE TOKENS (PT)";
        case 5:
            return "EXPEDITED (EX)";
        case 7:
            return "PREPARE (PR)";
        case 8:
            return "NOT FINISHED (NF)";
        case 9:
            return "FINISH (FN)";
        case 10:
            return "DISCONNECT (DN)";
        case 12:
            return "REFUSE (RF)";
        case 13:
            return "CONNECT (CN)";
        case 14:
            return "ACCEPT (AC)";
        case 15:
            return "CONNECT DATA OVERFLOW (CDO)";
        case 16:
            return "OVERFLOW ACCEPT (OA)";
        case 21:
            return "GIVE TOKENS CONFIRM (GTC)";
        case 22:
            return "GIVE TOKENS ACK (GTA)";
        case 25:
            return "ABORT (AB)";
        case 26:
            return "ABORT ACCEPT (AA)";
        case 29:
            return "ACTIVITY RESUME (AR)";
        case 33:
            return "TYPED DATA (TD)";
        case 34:
            return "RESYNCHRONIZE ACK (RA)";
        case 41:
            return "MAJOR SYNC POINT (MAP)";
        case 42:
            return "MAJOR SYNC ACK (MAA)";
        case 45:
            return "ACTIVITY START (AS)";
        case 48:
            return "EXCEPTION DATA (ED)";
        case 49:
            return "MINOR SYNC POINT (MIP)";
        case 50:
            return "MINOR SYNC ACK (MIA)";
        case 53:
            return "RESYNCHRONIZE (RS)";
        case 57:
            return "ACTIVITY DISCARD (AD)";
        case 58:
            return "ACTIVITY DISCARD ACK (ADA)";
        case 61:
            return "CAPABILITY DATA (CD)";
        case 62:
            return "CAPABILITY DATA ACK (CDA)";
        case 64:
            return "UNIT DATA (UD)";
        default:
            return "<unknown SPDU type>";
        }
    }
}
