/*
 * 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 javax.bluetooth.BluetoothStateException;
import javax.bluetooth.DiscoveryAgent;
import javax.bluetooth.DiscoveryListener;
import javax.bluetooth.RemoteDevice;
import javax.bluetooth.UUID;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Vector;

/*
 * The <code>DiscoveryAgentImpl</code> class is a DiscoveryAgent
 * API class implementation which does not extend this API class.
 */
public final class DiscoveryAgentImpl {
    /* Calls inquiry completion callback in a separate thread. */
    class Completed implements Runnable {
        /* type of completion. */
        private int discType;
        /* listener to be called. */
        private DiscoveryListener listener;
        /* Constructs an instance and starts the the thread. */
        Completed(DiscoveryListener listener, int discType) {
            this.listener = listener;
            this.discType = discType;
            new Thread(this).start();
        }
        /* Implements Runnable. */
        public void run() {
        	if (listener != null) {
        		listener.inquiryCompleted(discType);
        	}
        }
    }

    /* Set to false in RR version - then the javac skip the code. */
    private static final boolean DEBUG = false;

    /*
     * maximum number of allowed UUIDS in search uuids sequence
     */
    private static final int MAX_ALLOWED_UUIDS = 12;

    /*
     * Keeps an instance to the object of this class to be
     * accessible from the implementation.
     */
    private static DiscoveryAgentImpl instance;

    /* Keeps the <code>RemoteDeviceImpl</code> references of known devices. */
    private Hashtable knownDevices = new Hashtable();

    /* Keeps the <code>RemoteDeviceImpl</code> references of cached devices. */
    private Hashtable cachedDevices = new Hashtable();

    /*
     * Keeps the listener of the device discovery inquire.
     * Also, it is used as flag that a device is in inquire mode.
     */
    private DiscoveryListener d_listener;
    
    /* Keeps the lock object for device discovery synchronization. */
    private Object d_lock = new Object();

    /* Keeps the reference to module responsible for selecting services. */
    private SelectServiceHandler selectServiceHandler =
            new SelectServiceHandler(this);

    /* Constructs the single instance. */
    private DiscoveryAgentImpl() {}

    public RemoteDevice[] retrieveDevices(int option) {
        switch (option) {
            case DiscoveryAgent.CACHED:
                // IMPL_NOTE: use native cache keeping addresses of found devices
                // to share the cache between multiple isolates
                return getCachedDevices();
            case DiscoveryAgent.PREKNOWN:
                Vector pk = BCC.getInstance().getPreknownDevices();
                if (pk == null || pk.size() == 0) {
                    return null;
                }
                RemoteDevice[] res = new RemoteDevice[pk.size()];
                for (int i = 0; i < pk.size(); i++) {
                    String addr = (String)pk.elementAt(i);
                    res[i] = getRemoteDevice(addr);
                }
                return res;
            default:
                throw new IllegalArgumentException("Invalid option value: "
                        + option);
        }
    }


    /*
     * Adds address of remote device found during inquiry request to internal
     * inquiry cache.
     *
     * The method does nothing if the RemoteDevice is already in the cache.
     */
    public void addCachedDevice(String addr) {
        RemoteDevice rd = getRemoteDevice(addr);
        synchronized (cachedDevices) {
            cachedDevices.put(addr, rd);
        }
    }

    // JAVADOC COMMENT ELIDED
    private RemoteDevice[] getCachedDevices() {
        synchronized (cachedDevices) {
            int len = cachedDevices.size();
            if (len == 0) {
                return null;
            }
            RemoteDevice[] res = new RemoteDevice[len];
            Enumeration e = cachedDevices.elements();
            for (int i = 0; e.hasMoreElements(); i++) {
                res[i] = (RemoteDevice)e.nextElement();
            }
            return res;
        }
    }

    public boolean startInquiry(int accessCode, DiscoveryListener listener)
            throws BluetoothStateException {

        if (accessCode != DiscoveryAgent.GIAC &&
                accessCode != DiscoveryAgent.LIAC &&
                (accessCode < 0x9E8B00 || accessCode > 0x9E8B3F)) {
            throw new IllegalArgumentException("Access code is out of range: "
                    + accessCode);
        }

        if (listener == null) {
            throw new NullPointerException("null listener");
        }

        /* IMPL_NOTE see
        // kvem/classes/com/sun/kvem/jsr082/impl/bluetooth/
        //         BTDeviceDiscoverer.java
        // heck what access codes should be supported.
        // Return false if access code is not supported. 
         */

        synchronized (d_lock) {
            if (d_listener != null) {
                throw new BluetoothStateException(
                        "The previous device discovery is running...");
            }
            d_listener = listener;

            /* process the inquiry in the device specific way */
            return startInquiry(accessCode);
        }
    }

    private boolean startInquiry(int accessCode) throws BluetoothStateException {
        return BluetoothStack.getEnabledInstance().startInquiry(
            accessCode, d_listener);
    }

    public boolean cancelInquiry(DiscoveryListener listener) {
        if (listener == null) {
            throw new NullPointerException("null listener");
        }
        synchronized (d_lock) {

            /* no inquiry was started */
            if (d_listener == null) {
                return false;
            }

            /* not valid listener */
            if (d_listener != listener) {
                return false;
            }

            /* process the inquiry in the device specific way */
            cancelInquiry();
        }

        inquiryCompleted(DiscoveryListener.INQUIRY_TERMINATED);
        return true;
    }

    /*
     * Cancels inquiry in device specific way.
     */
    private void cancelInquiry() {
        BluetoothStack.getInstance().cancelInquiry(d_listener);
    }

    /*
     * Porting interface: this method is used by the device specific
     * implementation to notify this class, that the current inquire
     * has been completed.
     *
     * @param discType type of completion:
     * <code>DiscoveryListener.INQUIRY_COMPLETED</code>, or
     * <code>DiscoveryListener.INQUIRY_TERMINATED</code>, or
     * <code>DiscoveryListener.INQUIRY_ERROR</code>
     */
    public void inquiryCompleted(int discType) {
        DiscoveryListener listener;
        synchronized (d_lock) {
            listener = d_listener;
            d_listener = null;
        }

        new Completed(listener, discType);
    }

    /*
     * Porting interface: this method is used by the device specific
     * implementation to create the RemoteDevice object by address.
     *
     * Also, this method puts the new remote devices into cache of
     * known devices.
     *
     * @param addr address of remote device to be created
     *
     * @return new <code>RemoteDeviceImpl</code>instance if device with address
     * given is unknown, the known one otherwise.
     */
    public RemoteDeviceImpl getRemoteDevice(String addr) {
        synchronized (knownDevices) {
            addr = addr.toUpperCase();
            RemoteDeviceImpl rd = (RemoteDeviceImpl) knownDevices.get(addr);

            if (rd == null) {
                rd = new RemoteDeviceImpl(addr);
                knownDevices.put(addr, rd);
            }
            return rd;
        }
    }

    public int searchServices(int[] attrSet, UUID[] uuidSet, RemoteDevice btDev,
            DiscoveryListener discListener) throws BluetoothStateException {

        if (DEBUG) {
            System.out.println("searchServices: ");
            System.out.println("\tattrSet=" + attrSet);

            if (attrSet != null) {
                for (int i = 0; i < attrSet.length; i++) {
                    System.out.println("\tattrSet[" + i + "]=0x" + attrSet[i]);
                }
            }
            System.out.println("\tuuidSet=" + uuidSet);

            if (uuidSet != null) {
                for (int i = 0; i < uuidSet.length; i++) {
                    System.out.println("\tuuidSet[" + i + "]=" + uuidSet[i]);
                }
            }
            System.out.println("\tadderess=" + btDev.getBluetoothAddress());
        }
        if (uuidSet == null) {
            throw new NullPointerException("UUID set is null");
        }

        if (uuidSet.length == 0 || uuidSet.length > MAX_ALLOWED_UUIDS ) {
            throw new IllegalArgumentException("Invalid UUID set length");
        }

        if (btDev == null) {
            throw new NullPointerException("null instance of RemoteDevice");
        }
        
        /* the 'transID' is assigned by service discoverer */
        int transID = ServiceDiscovererFactory.getServiceDiscoverer().
                searchService(
                        ServiceSearcherBase.extendByStandardAttrs(attrSet), 
                        ServiceSearcherBase.removeDuplicatedUuids(uuidSet), 
                        btDev, discListener);

        if (DEBUG) {
            System.out.println("\ttransID=" + transID);
        }
        return transID;
    }

    public boolean cancelServiceSearch(int transID) {
        if (DEBUG) {
            System.out.println("cancelServiceSearch: transID=" + transID);
        }
        return ServiceDiscovererFactory.getServiceDiscoverer().cancel(transID);
    }

    public String selectService(UUID uuid, int security, boolean master)
            throws BluetoothStateException {

        // use the separated class to light this one
        return selectServiceHandler.selectService(uuid, security, master);
//        return ServiceDiscovererFactory.getServiceDiscoverer().
//                                 selectService(uuid, security, master, this);
    }

    /*
     * Returns the instance of this singleton constructing it if needed.
     * @return the only instance of <code>DiscoveryAgentImpl</code>.
     */
    public static synchronized DiscoveryAgentImpl getInstance() {
        if (instance == null) {
            instance = new DiscoveryAgentImpl();
        }

        return instance;
    }
}