/*
 * Copyright  1990-2009 Sun Microsystems, Inc. All Rights Reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License version
 * 2 only, as published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * General Public License version 2 for more details (a copy is
 * included at /legal/license.txt).
 *
 * You should have received a copy of the GNU General Public License
 * version 2 along with this work; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA
 *
 * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa
 * Clara, CA 95054 or visit www.sun.com if you need additional
 * information or have any questions.
 */
package com.sun.jsr082.bluetooth;

import java.io.IOException;
import java.util.Enumeration;
import java.util.Hashtable;
import javax.bluetooth.BluetoothStateException;
import javax.bluetooth.DataElement;
import javax.bluetooth.LocalDevice;
import javax.bluetooth.RemoteDevice;
import javax.bluetooth.ServiceRecord;
import javax.bluetooth.UUID;

import com.sun.jsr082.bluetooth.SDPClient;

/*
 * Service record implementation.
 */
public final class ServiceRecordImpl implements ServiceRecord {

    /* Maxumum quantity of attributes in one request */
    static final int RETRIEVABLE_MAX;

    /*
     * Maximum number of concurrent service searches that can
     * exist at any one time.
     */
    static final int TRANS_MAX;

    /* Remote device service provided by. */
    private RemoteDevice remoteDevice = null;

    /* Service notifier. */
    private BluetoothNotifier notifier = null;

    /* Attribues of the record. */
    private Hashtable attributesTable = null;

    /* Bit scale that keeps service classes. */
    private int serviceClasses = 0;

    /* Mask to identify attribute IDs out of range. */
    private static final int MASK_OVERFLOW = 0xffff0000;

    /* Mask of incorrect class bits. */
    private static final int MASK_INCORRECT_CLASS = 0xff003fff;

    /* ServiceRecordHandle attribute ID. */
    public static final int SERVICE_RECORD_HANDLE = 0x0000;

    /* ProtocolDescriptorList attribute ID. */
    public static final int PROTOCOL_DESCRIPTOR_LIST = 0x0004;

    /* Service class attribute id. */
    public static final int SERVICE_CLASS_ATTR_ID = 0x0001;

    /* Name attribute id. */
    public static final int NAME_ATTR_ID = 0x0100;

    /* Protocol type. */
    private int protocol = BluetoothUrl.UNKNOWN;

    /* Bluetooth address of device service record came from. */
    private String btaddr = null;

    /* PSM or channel id. */
    private int port = -1;

    /* Record handle */
    private int recHandle = 0;
    /* SDPClient from where this ServiceRecord is created */
    public SDPClient sdpClient = null;
    
    static {
        int retrievableMax = 5; // default value
        try {
            retrievableMax = Integer.parseInt(LocalDevice.getProperty(
                "bluetooth.sd.attr.retrievable.max"));
        } catch (NumberFormatException e) {
            System.err.println("Internal error: ServiceRecordImpl: "
                    + "improper retrievable.max value");
        }
        RETRIEVABLE_MAX = retrievableMax;

        int transMax = 10;  // default value
        try {
            transMax = Integer.parseInt(LocalDevice.getProperty(
                "bluetooth.sd.trans.max"));
        } catch (NumberFormatException e) {
            System.err.println("Internal error: ServiceRecordImpl: "
                    + "improper trans.max value");
        }
        TRANS_MAX = transMax;
    }

    /*
     * Creates service records on client device.
     *
     * @param device server device
     * @param attrIDs attributes IDs
     * @param attrValues attributes values
     */
    public ServiceRecordImpl(RemoteDevice device, int[] attrIDs,
            DataElement[] attrValues) {
        init(attrIDs, attrValues);
        remoteDevice = device;
    }


    /*
     * Creates service records for the given notifier.
     *
     * @param notifier notifier to be associated with this service record
     * @param attrIDs attributes IDs
     * @param attrValues attributes values
     */
    public ServiceRecordImpl(BluetoothNotifier notifier, int[] attrIDs,
            DataElement[] attrValues) {
        init(attrIDs, attrValues);
        this.notifier = notifier;
    }

    /*
     * Creates a copy of this record. The copy recieves new instances of
     * attributes values which are of types <code>DataElement.DATSEQ</code>
     * or <code>DataElement.DATALT</code> (the only data element types that
     * can be modified after creation).
     *
     * @return new instance, a copy of this one.
     */
    public synchronized ServiceRecordImpl copy() {
        int count = attributesTable.size();
        int[] attrIDs = new int[count];
        DataElement[] attrValues = new DataElement[count];

        Enumeration ids = attributesTable.keys();
        Enumeration values = attributesTable.elements();

        for (int i = 0; i < count; i++) {
            attrIDs[i] = ((Integer)ids.nextElement()).intValue();
            // no nedd to copy elements here; service record constructor
            // performs the copying
            attrValues[i] = (DataElement)values.nextElement();
        }

        ServiceRecordImpl servRec = new ServiceRecordImpl(notifier,
                      attrIDs, attrValues);
        servRec.serviceClasses = serviceClasses;
        return servRec;
    }

    /*
     * Returns service record handle.
     *
     * @return service record handle, or 0 if the record is not in SDDB.
     */
    public int getHandle() {
        DataElement handle = getAttributeValue(SERVICE_RECORD_HANDLE);
        return handle != null ? (int)handle.getLong() : 0;
    }

    /*
     * Sets service record handle.
     *
     * @param handle new service record handle value
     */
    public void setHandle(int handle) {
        Integer attrID = new Integer(SERVICE_RECORD_HANDLE);
        attributesTable.remove(attrID);
        attributesTable.put(attrID, new DataElement(
                DataElement.U_INT_4, handle));
        recHandle = handle;
    }

    /*
     * Returns notifier that has created this record.
     * @return corresponding notifier.
     */
    public BluetoothNotifier getNotifier() {
        return notifier;
    }

    /*
     * Creates attributes table and fills it up by values given.
     * @param attrIDs attributes IDs
     * @param attrValues attributes values
     */
    private void init(int[] attrIDs, DataElement[] attrValues) {
        attributesTable = new Hashtable(attrIDs.length + 1);
        attrsInit(attrIDs, attrValues);
    }

    /*
     * Fills up attributes table by values given.
     * @param attrIDs attributes IDs
     * @param attrValues attributes values
     */
    private void attrsInit(int[] attrIDs, DataElement[] attrValues) {
        for (int i = 0; i < attrIDs.length; i++) {
            attributesTable.put(new Integer(attrIDs[i]),
                    dataElementCopy(attrValues[i]));
        }
    }

    /*
     * Creates a copy of DataElement if it's necessary.
     * @param original data element to be copied if its type
     *        allows value modification
     * @return copy of data element
     */
    private DataElement dataElementCopy(DataElement original) {
        if ((original.getDataType() == DataElement.DATSEQ)
                || (original.getDataType() == DataElement.DATALT)) {
            DataElement copy = new DataElement(original.getDataType());
            Enumeration elements = (Enumeration) original.getValue();

            while (elements.hasMoreElements()) {
                copy.addElement(dataElementCopy((DataElement)
                        elements.nextElement()));
            }
            return copy;
        } else {
            return original;
        }
    }

    // JAVADOC COMMENT ELIDED
    public DataElement getAttributeValue(int attrID) {
        if ((attrID & MASK_OVERFLOW) != 0) {
            throw new IllegalArgumentException(
                    "attrID isn't a 16-bit unsigned integer");
        }
        DataElement attrValue = (DataElement) attributesTable.get(new
                Integer(attrID));

        if (attrValue == null) {
            return null;
        } else {
            return dataElementCopy(attrValue);
        }
    }

    // JAVADOC COMMENT ELIDED
    public RemoteDevice getHostDevice() {
        return remoteDevice;
    }

    // JAVADOC COMMENT ELIDED
    public synchronized int[] getAttributeIDs() {
        int[] attrIDs = new int[attributesTable.size()];
        Enumeration e = attributesTable.keys();

        for (int i = 0; i < attrIDs.length; i++) {
            attrIDs[i] = ((Integer) e.nextElement()).intValue();
        }
        return attrIDs;
    }

    // JAVADOC COMMENT ELIDED
    public synchronized boolean populateRecord(int[] attrIDs)
        throws IOException {
        Hashtable dupChecker = new Hashtable();
        Object checkObj = new Object();

        if (remoteDevice == null) {
            throw new RuntimeException("local ServiceRecord");
        }

        if (attrIDs.length == 0) {
            throw new IllegalArgumentException("attrIDs size is zero");
        }

        if (attrIDs.length > RETRIEVABLE_MAX) {
            throw new IllegalArgumentException(
                    "attrIDs size exceeds retrievable.max");
        }

        for (int i = 0; i < attrIDs.length; i++) {
            if ((attrIDs[i] & MASK_OVERFLOW) != 0) {
                throw new IllegalArgumentException("attrID does not represent "
                    + "a 16-bit unsigned integer");
            }

            // check attribute ID duplication
            if (dupChecker.put(new Integer(attrIDs[i]), checkObj) != null) {
                throw new IllegalArgumentException(
                        "duplicated attribute ID");
            }
        }

        // obtains transaction ID for request
        short transactionID = SDPClientTransactionBase.newTransactionID();

        // SDP connection and listener. They are initialized in try blok.
        SDPClient sdp = null;
        SRSDPListener listener = null;

        try {
            // prepare data for request
            DataElement handleEl = (DataElement) attributesTable.get(
                    new Integer(SERVICE_RECORD_HANDLE));
            int handle = (int) handleEl.getLong();

            // create and prepare SDP listner
            listener = new SRSDPListener();

            // create SDP connection and ..
            if (sdpClient == null) {
                sdp = ServiceDiscovererFactory.getServiceDiscoverer().
                        getSDPClient(remoteDevice.getBluetoothAddress());
            } else {
                sdp = sdpClient;
            }

            // ... and make request
            sdp.serviceAttributeRequest(handle, attrIDs, transactionID,
                    listener);

            synchronized (listener) {
                if ((listener.ioExcpt == null)
                        && (listener.attrValues == null)) {
                    try {
                        listener.wait();
                    } catch (InterruptedException ie) {
                        // ignore (breake waiting)
                    }
                }
            }
        } finally {

            // Closes SDP connection and frees transaction ID in any case
            SDPClientTransactionBase.freeTransactionID(transactionID);

            // if connection was created try to close it
            if (sdp != null) {
                try {
                    sdp.close();
                } catch (IOException ioe) {
                    // ignore
                }
            }
        }

        if (listener.ioExcpt != null) {
            throw listener.ioExcpt;
        } else if (listener.attrValues == null) {
            return false;
        } else if (listener.attrValues.length == 0) {
            return false;
        } else {
            attrsInit(listener.attrIDs, listener.attrValues);
            return true;
        }
    }

    // JAVADOC COMMENT ELIDED
    public synchronized String getConnectionURL(int requiredSecurity,
                                                boolean mustBeMaster) {
        // protocol, btaddr, port
        retrieveUrlCommonParams();
        if (protocol == BluetoothUrl.UNKNOWN) {
        	return null;
        }
        BluetoothUrl url = BluetoothUrl.createClientUrl(
                protocol, btaddr, port);

        if (mustBeMaster) {
            url.master = true;
        } else {
            url.master = false;
        }

        switch (requiredSecurity) {
        case NOAUTHENTICATE_NOENCRYPT:
            break;
        case AUTHENTICATE_ENCRYPT:
            url.encrypt = true;
        case AUTHENTICATE_NOENCRYPT:
            url.authenticate = true;
            break;
        default:
            throw new IllegalArgumentException("unsupported security type: "
                    + requiredSecurity);
        }

        return url.toString();
    }

    /*
     * Retrieves service protocol, device address and port (PSM or channel)
     * from service record attributes. Results are set to
     * <code>protocol</code>, <code>btaddr</code> and <code>port</code>
     * variables correspondingly.
     */
    private void retrieveUrlCommonParams() {
        if (protocol != BluetoothUrl.UNKNOWN) {
            // already retrieved
            return;
        }

        if (remoteDevice != null) {
            btaddr = remoteDevice.getBluetoothAddress();
        } else {
            try {
                btaddr = LocalDevice.getLocalDevice().getBluetoothAddress();
            } catch (BluetoothStateException bse) {
                throw new IllegalArgumentException("cannot generate url");
            }
        }

        /*
         * There are three protocols supported -
         * they are obex or rfcomm or l2cap. So, if obex is
         * found in ProtocolDescriptorList, the protocol is btgoep,
         * if RFCOMM is found (and no obex) - the btspp, otherwise
         * the protocol is btl2cap.
         */
        DataElement protocolList = getAttributeValue(PROTOCOL_DESCRIPTOR_LIST);
        if (protocolList == null) {
        	return;
        }
        Enumeration val = (Enumeration) protocolList.getValue();
        int type = -1; // 0 = l2cap, 1 = spp, 2 = obex
        final UUID L2CAP_UUID = new UUID(0x0100);
        final UUID RFCOMM_UUID = new UUID(0x0003);
        final UUID OBEX_UUID = new UUID(0x0008);

        // go through all of the protocols in the protocols list
        while (val.hasMoreElements()) {
            DataElement protoDE = (DataElement) val.nextElement();

            // application adds a garbage in protocolList - ignore
            if (protoDE.getDataType() != DataElement.DATSEQ) {
                continue;
            }
            Enumeration protoEnum = (Enumeration) protoDE.getValue();
            int tmpPort = -1;
            int tmpType = -1;

            // look on protocol details
            while (protoEnum.hasMoreElements()) {
                DataElement de = (DataElement) protoEnum.nextElement();

                // may be PSM or channel id
                if (de.getDataType() == DataElement.U_INT_1 ||
                        de.getDataType() == DataElement.U_INT_2)  {
                    tmpPort = (int) de.getLong();
                } else if (de.getDataType() == DataElement.UUID) {
                    UUID protoUUID = (UUID) de.getValue();

                    if (protoUUID.equals(L2CAP_UUID)) {
                        tmpType = 0;
                    } else if (protoUUID.equals(RFCOMM_UUID)) {
                        tmpType = 1;
                    } else if (protoUUID.equals(OBEX_UUID)) {
                        tmpType = 2;
                    }
                }
            }

            /*
             * ok, new protocol has been parsed - let's check if it
             * is over the previous one or not.
             *
             * Note, that OBEX protocol may appear before the RFCOMM
             * one - in this case the port (channel id) is not set -
             * need to check this case separately.
             */
            if (tmpType > type) {
                type = tmpType;

                // no "port" for obex type (obex = 2)
                if (tmpType != 2) {
                    port = tmpPort;
                }
            } else if (tmpType == 1) {
                port = tmpPort;
            }
        }

        switch (type) {
        case 0:
            protocol = BluetoothUrl.L2CAP;
            break;
        case 1:
            protocol = BluetoothUrl.RFCOMM;
            break;
        case 2:
            protocol = BluetoothUrl.OBEX;
            break;
        default:
            throw new IllegalArgumentException("wrong protocol list");
        }
    }

    /*
     * Retrieve service classes bits provided by corresponing service
     * at local device.
     *
     * @return an integer that keeps the service classes bits
     */
    public int getDeviceServiceClasses() {
        if (remoteDevice != null) {
            throw new RuntimeException(
                "This ServiceRecord was created by a call to "
                + "DiscoveryAgent.searchServices()");
        }

        // it's necessary to improve these code
        return serviceClasses;
    }

    // JAVADOC COMMENT ELIDED
    public synchronized void setDeviceServiceClasses(int classes) {
        // checks that it's service record from remote device
        if (remoteDevice != null) {
            throw new RuntimeException("This ServiceRecord was created"
                    + " by a call to DiscoveryAgent.searchServices()");
        }

        // checks correction of set classbits
        if ((classes & MASK_INCORRECT_CLASS) != 0) {
            throw new IllegalArgumentException("attempt to set incorrect bits");
        }
        serviceClasses = classes;
    }

    // JAVADOC COMMENT ELIDED
    public synchronized boolean setAttributeValue(
            int attrID, DataElement attrValue) {

        if ((attrID & MASK_OVERFLOW) != 0) {
            throw new IllegalArgumentException(
                    "attrID does not represent a 16-bit unsigned integer");
        }

        if (attrID == SERVICE_RECORD_HANDLE) {
            throw new IllegalArgumentException(
                    "attrID is the value of ServiceRecordHandle (0x0000)");
        }

        if (remoteDevice != null) {
            throw new RuntimeException(
                    "can't update ServiceRecord of the RemoteDevice");
        }
        Object key = new Integer(attrID);

        if (attrValue == null) {
            return attributesTable.remove(key) != null;
        } else {
            attributesTable.put(key, dataElementCopy(attrValue));
            return true;
        }
    }

    /*
     * SDP responce listener that is used within <code>populateRecord()</code>
     * processing.
     */
    class SRSDPListener implements SDPResponseListener {
        /* Attributes values retrieved form remote device. */
        DataElement[] attrValues = null;
        /* Keeps an IOException to be thrown. */
        IOException ioExcpt = null;
        /* IDs of attributes to be retrieved. */
        int[] attrIDs = null;

        /*
         * Receives error response.
         * @param errorCode error code
         * @param info error information
         * @param transactionID transaction ID
         */
        public void errorResponse(int errorCode, String info,
                int transactionID) {
            synchronized (this) {
                ioExcpt = new IOException(info);
                notify();
            }
        }

        /*
         * Implements required SDPResponseListener method,
         * must not be called.
         * @param handleList no matter
         * @param transactionID no matter
         */
        public void serviceSearchResponse(int[] handleList,
                int transactionID) {
            throw new RuntimeException("unexpected call");
        }

        /*
         * Receives arrays of service record attributes and their values.
         * @param attributeIDs list of attributes whose values were requested
         *        from server.
         * @param attributeValues values returned by server within.
         * @param transactionID ID of transaction response recieved within.
         */
        public void serviceAttributeResponse(int[] attributeIDs,
            DataElement[] attributeValues, int transactionID) {
            synchronized (this) {
                attrIDs = attributeIDs;
                attrValues = attributeValues;
                notify();
            }
        }

        /*
         * Implements required SDPResponseListener method,
         * must not be called.
         * @param attrIDs no matter
         * @param attributeValues no matter
         * @param transactionID no matter
         */
        public void serviceSearchAttributeResponse(int[] attrIDs,
            DataElement[] attributeValues, int transactionID) {
            throw new RuntimeException("unexpected call");
        }
    }
}