/*
 * Copyright (C) 2017 The Android Open Source Project
 *
 * 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 android.net.lowpan;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.net.IpPrefix;
import android.os.Handler;
import android.os.IBinder;
import android.os.RemoteException;
import android.os.ServiceSpecificException;
import android.util.Log;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

/**
 * Class for managing a specific Low-power Wireless Personal Area Network (LoWPAN) interface.
 *
 * @hide
 */
//@SystemApi
public class LowpanInterface {
    private static final String TAG = LowpanInterface.class.getSimpleName();

    /** Detached role. The interface is not currently attached to a network. */
    public static final String ROLE_DETACHED = "detached";

    /** End-device role. End devices do not route traffic for other nodes. */
    public static final String ROLE_END_DEVICE = "end-device";

    /** Router role. Routers help route traffic around the mesh network. */
    public static final String ROLE_ROUTER = "router";

    /**
     * Sleepy End-Device role.
     *
     * <p>End devices with this role are nominally asleep, waking up periodically to check in with
     * their parent to see if there are packets destined for them. Such devices are capable of
     * extraordinarilly low power consumption, but packet latency can be on the order of dozens of
     * seconds(depending on how the node is configured).
     */
    public static final String ROLE_SLEEPY_END_DEVICE = "sleepy-end-device";

    /**
     * Sleepy-router role.
     *
     * <p>Routers with this role are nominally asleep, waking up periodically to check in with other
     * routers and their children.
     */
    public static final String ROLE_SLEEPY_ROUTER = "sleepy-router";

    /** TODO: doc */
    public static final String ROLE_LEADER = "leader";

    /** TODO: doc */
    public static final String ROLE_COORDINATOR = "coordinator";

    /**
     * Offline state.
     *
     * <p>This is the initial state of the LoWPAN interface when the underlying driver starts. In
     * this state the NCP is idle and not connected to any network.
     *
     * <p>This state can be explicitly entered by calling {@link #reset()}, {@link #leave()}, or
     * <code>setUp(false)</code>, with the later two only working if we were not previously in the
     * {@link #STATE_FAULT} state.
     *
     * @see #getState()
     * @see #STATE_FAULT
     */
    public static final String STATE_OFFLINE = "offline";

    /**
     * Commissioning state.
     *
     * <p>The interface enters this state after a call to {@link #startCommissioningSession()}. This
     * state may only be entered directly from the {@link #STATE_OFFLINE} state.
     *
     * @see #startCommissioningSession()
     * @see #getState()
     * @hide
     */
    public static final String STATE_COMMISSIONING = "commissioning";

    /**
     * Attaching state.
     *
     * <p>The interface enters this state when it starts the process of trying to find other nodes
     * so that it can attach to any pre-existing network fragment, or when it is in the process of
     * calculating the optimal values for unspecified parameters when forming a new network.
     *
     * <p>The interface may stay in this state for a prolonged period of time (or may spontaneously
     * enter this state from {@link #STATE_ATTACHED}) if the underlying network technology is
     * heirarchical (like ZigBeeIP) or if the device role is that of an "end-device" ({@link
     * #ROLE_END_DEVICE} or {@link #ROLE_SLEEPY_END_DEVICE}). This is because such roles cannot
     * create their own network fragments.
     *
     * @see #STATE_ATTACHED
     * @see #getState()
     */
    public static final String STATE_ATTACHING = "attaching";

    /**
     * Attached state.
     *
     * <p>The interface enters this state from {@link #STATE_ATTACHING} once it is actively
     * participating on a network fragment.
     *
     * @see #STATE_ATTACHING
     * @see #getState()
     */
    public static final String STATE_ATTACHED = "attached";

    /**
     * Fault state.
     *
     * <p>The interface will enter this state when the driver has detected some sort of problem from
     * which it was not immediately able to recover.
     *
     * <p>This state can be entered spontaneously from any other state. Calling {@link #reset} will
     * cause the device to return to the {@link #STATE_OFFLINE} state.
     *
     * @see #getState
     * @see #STATE_OFFLINE
     */
    public static final String STATE_FAULT = "fault";

    /**
     * Network type for Thread 1.x networks.
     *
     * @see android.net.lowpan.LowpanIdentity#getType
     * @see #getLowpanIdentity
     * @hide
     */
    public static final String NETWORK_TYPE_THREAD = "org.threadgroup.thread.v1";

    /**
     * Network type for ZigBeeIP 1.x networks.
     *
     * @see android.net.lowpan.LowpanIdentity#getType
     * @see #getLowpanIdentity
     * @hide
     */
    public static final String NETWORK_TYPE_ZIGBEE_IP = "org.zigbee.zigbeeip.v1";

    private static final String NETWORK_PROPERTY_KEYS[] = {
        LowpanProperties.KEY_NETWORK_NAME.getName(),
        LowpanProperties.KEY_NETWORK_PANID.getName(),
        LowpanProperties.KEY_NETWORK_XPANID.getName(),
        LowpanProperties.KEY_CHANNEL.getName()
    };

    /**
     * Callback base class for LowpanInterface
     *
     * @hide
     */
    //@SystemApi
    public abstract static class Callback {
        public void onConnectedChanged(boolean value) {}

        public void onEnabledChanged(boolean value) {}

        public void onUpChanged(boolean value) {}

        public void onRoleChanged(@NonNull String value) {}

        public void onStateChanged(@NonNull String state) {}

        public void onLowpanIdentityChanged(@NonNull LowpanIdentity value) {}

        /** @hide */
        public void onPropertiesChanged(@NonNull Map properties) {}
    }

    private ILowpanInterface mBinder;
    private final HashMap<Integer, ILowpanInterfaceListener> mListenerMap = new HashMap<>();

    /** Map between IBinder identity hashes and LowpanInstance objects. */
    private static final HashMap<Integer, LowpanInterface> sInstanceMap = new HashMap<>();

    private LowpanInterface(IBinder binder) {
        mBinder = ILowpanInterface.Stub.asInterface(binder);
    }

    /**
     * Get the LowpanInterface object associated with this IBinder. Returns null if this IBinder
     * does not implement the appropriate interface.
     *
     * @hide
     */
    @NonNull
    public static final LowpanInterface from(IBinder binder) {
        Integer hashCode = Integer.valueOf(System.identityHashCode(binder));
        LowpanInterface instance;

        synchronized (sInstanceMap) {
            instance = sInstanceMap.get(hashCode);

            if (instance == null) {
                instance = new LowpanInterface(binder);
                sInstanceMap.put(hashCode, instance);
            }
        }

        return instance;
    }

    /** {@hide} */
    public static final LowpanInterface from(ILowpanInterface iface) {
        return from(iface.asBinder());
    }

    /** {@hide} */
    public static final LowpanInterface getInterfaceFromBinder(IBinder binder) {
        return from(binder);
    }

    /**
     * Returns the IBinder object associated with this interface.
     *
     * @hide
     */
    public IBinder getBinder() {
        return mBinder.asBinder();
    }

    private static void throwAsPublicException(Throwable t) throws LowpanException {
        LowpanException.throwAsPublicException(t);
    }

    //////////////////////////////////////////////////////////////////////////
    // Private Property Helpers

    void setProperties(Map properties) throws LowpanException {
        try {
            mBinder.setProperties(properties);

        } catch (RemoteException x) {
            // Catch and ignore all binder exceptions
            Log.e(TAG, x.toString());

        } catch (ServiceSpecificException x) {
            throwAsPublicException(x);
        }
    }

    @NonNull
    Map<String, Object> getProperties(String keys[]) throws LowpanException {
        try {
            return mBinder.getProperties(keys);
        } catch (RemoteException x) {
            // Catch and ignore all binder exceptions
            Log.e(TAG, x.toString());
        } catch (ServiceSpecificException x) {
            throwAsPublicException(x);
        }
        return new HashMap();
    }

    /** @hide */
    public <T> void setProperty(LowpanProperty<T> key, T value) throws LowpanException {
        HashMap<String, T> prop = new HashMap<>();
        prop.put(key.getName(), value);
        setProperties(prop);
    }

    /** @hide */
    @Nullable
    public <T> T getProperty(LowpanProperty<T> key) throws LowpanException {
        Map<String, Object> map = getProperties(new String[] {key.getName()});
        if (map != null && !map.isEmpty()) {
            // We know there is only one value.
            return (T) map.values().iterator().next();
        }
        return null;
    }

    @Nullable
    <T> String getPropertyAsString(LowpanProperty<T> key) throws LowpanException {
        try {
            return mBinder.getPropertyAsString(key.getName());
        } catch (RemoteException x) {
            // Catch and ignore all binder exceptions
            Log.e(TAG, x.toString());
        } catch (ServiceSpecificException x) {
            throwAsPublicException(x);
        }
        return null;
    }

    int getPropertyAsInt(LowpanProperty<Integer> key) throws LowpanException {
        Integer value = getProperty(key);
        return (value != null) ? value : 0;
    }

    boolean getPropertyAsBoolean(LowpanProperty<Boolean> key) throws LowpanException {
        Boolean value = getProperty(key);
        return (value != null) ? value : 0;
    }

    //////////////////////////////////////////////////////////////////////////
    // Public Actions

    /**
     * Form a new network with the given network information optional credential. Unspecified fields
     * in the network information will be filled in with reasonable values. If the network
     * credential is unspecified, one will be generated automatically.
     *
     * <p>This method will block until either the network was successfully formed or an error
     * prevents the network form being formed.
     *
     * <p>Upon success, the interface will be up and attached to the newly formed network.
     *
     * @see #join(LowpanProvision)
     */
    public void form(@NonNull LowpanProvision provision) throws LowpanException {
        try {
            Map<String, Object> parameters = new HashMap();
            provision.addToMap(parameters);
            mBinder.form(parameters);
        } catch (RemoteException x) {
            throwAsPublicException(x);
        } catch (ServiceSpecificException x) {
            throwAsPublicException(x);
        }
    }

    /**
     * Attempts to join a new network with the given network information. This method will block
     * until either the network was successfully joined or an error prevented the network from being
     * formed. Upon success, the interface will be up and attached to the newly joined network.
     *
     * <p>Note that “joining” is distinct from “attaching”: Joining requires at least one other peer
     * device to be present in order for the operation to complete successfully.
     */
    public void join(@NonNull LowpanProvision provision) throws LowpanException {
        try {
            Map<String, Object> parameters = new HashMap();
            provision.addToMap(parameters);
            mBinder.join(parameters);
        } catch (RemoteException x) {
            throwAsPublicException(x);
        } catch (ServiceSpecificException x) {
            throwAsPublicException(x);
        }
    }

    /**
     * Attaches to the network described by identity and credential. This is similar to {@link
     * #join}, except that (assuming the identity and credential are valid) it will always succeed
     * and provision the interface, even if there are no peers nearby.
     *
     * <p>This method will block execution until the operation has completed.
     */
    public void attach(@NonNull LowpanProvision provision) throws LowpanException {
        if (ROLE_DETACHED.equals(getRole())) {
            Map<String, Object> parameters = new HashMap();
            provision.addToMap(parameters);
            setProperties(parameters);
            setUp(true);
        } else {
            throw new LowpanException(LowpanException.LOWPAN_ALREADY);
        }
    }

    /**
     * Bring down the network interface and forget all non-volatile details about the current
     * network.
     *
     * <p>This method will block execution until the operation has completed.
     */
    public void leave() throws LowpanException {
        try {
            mBinder.leave();
        } catch (RemoteException x) {
            throwAsPublicException(x);
        } catch (ServiceSpecificException x) {
            throwAsPublicException(x);
        }
    }

    /**
     * Start a new commissioning session. Will fail if the interface is attached to a network or if
     * the interface is disabled.
     */
    public @NonNull LowpanCommissioningSession startCommissioningSession(
            @NonNull LowpanBeaconInfo beaconInfo) throws LowpanException {

        /* TODO: Implement startCommissioningSession */
        throw new LowpanException(LowpanException.LOWPAN_FEATURE_NOT_SUPPORTED);
    }

    /**
     * Reset this network interface as if it has been power cycled. Will bring the network interface
     * down if it was previously up. Will not erase any non-volatile settings.
     *
     * <p>This method will block execution until the operation has completed.
     *
     * @hide
     */
    public void reset() throws LowpanException {
        try {
            mBinder.reset();
        } catch (RemoteException x) {
            throwAsPublicException(x);
        } catch (ServiceSpecificException x) {
            throwAsPublicException(x);
        }
    }

    //////////////////////////////////////////////////////////////////////////
    // Public Getters and Setters

    /**
     * Returns the name of this network interface.
     *
     * <p>Will return empty string if this interface is no longer viable.
     */
    @NonNull
    public String getName() {
        try {
            return mBinder.getName();
        } catch (RemoteException x) {
            // Catch and ignore all binder exceptions
            // when fetching the name.
            Log.e(TAG, x.toString());
        } catch (ServiceSpecificException x) {
            // Catch and ignore all service-specific exceptions
            // when fetching the name.
            Log.e(TAG, x.toString());
        }
        return "";
    }

  /**
   * Indicates if the interface is enabled or disabled.
   *
   * @see #setEnabled
   * @see android.net.lowpan.LowpanException#LOWPAN_DISABLED
   */
  public boolean isEnabled() {
        try {
            return getPropertyAsBoolean(LowpanProperties.KEY_INTERFACE_ENABLED);
        } catch (LowpanException x) {
            return false;
        }
    }

  /**
   * Enables or disables the LoWPAN interface. When disabled, the interface is put into a low-power
   * state and all commands that require the NCP to be queried will fail with {@link
   * android.net.lowpan.LowpanException#LOWPAN_DISABLED}.
   *
   * @see #isEnabled
   * @see android.net.lowpan.LowpanException#LOWPAN_DISABLED
   * @hide
   */
  public void setEnabled(boolean enabled) throws LowpanException {
        setProperty(LowpanProperties.KEY_INTERFACE_ENABLED, enabled);
    }

    /**
     * Indicates if the network interface is up or down.
     *
     * @hide
     */
    public boolean isUp() {
        try {
            return getPropertyAsBoolean(LowpanProperties.KEY_INTERFACE_UP);
        } catch (LowpanException x) {
            return false;
        }
    }

    /**
     * Bring up or shut down the network interface.
     *
     * <p>This method brings up or shuts down the network interface, attaching or (gracefully)
     * detaching from the currently configured LoWPAN network as appropriate.
     *
     * @hide
     */
    public void setUp(boolean interfaceUp) throws LowpanException {
        setProperty(LowpanProperties.KEY_INTERFACE_UP, interfaceUp);
    }

    /**
     * Indicates if there is at least one peer in range.
     *
     * @return <code>true</code> if we have at least one other peer in range, <code>false</code>
     *     otherwise.
     */
    public boolean isConnected() {
        try {
            return getPropertyAsBoolean(LowpanProperties.KEY_INTERFACE_CONNECTED);
        } catch (LowpanException x) {
            return false;
        }
    }

    /**
     * Indicates if this interface is currently commissioned onto an existing network. If the
     * interface is commissioned, the interface may be brought up using setUp().
     */
    public boolean isCommissioned() {
        try {
            return getPropertyAsBoolean(LowpanProperties.KEY_INTERFACE_COMMISSIONED);
        } catch (LowpanException x) {
            return false;
        }
    }

    /**
     * Get interface state
     *
     * <h3>State Diagram</h3>
     *
     * <img src="LowpanInterface-1.png" />
     *
     * @return The current state of the interface.
     * @see #STATE_OFFLINE
     * @see #STATE_COMMISSIONING
     * @see #STATE_ATTACHING
     * @see #STATE_ATTACHED
     * @see #STATE_FAULT
     */
    public String getState() {
        try {
            return getProperty(LowpanProperties.KEY_INTERFACE_STATE);
        } catch (LowpanException x) {
            Log.e(TAG, x.toString());
            return STATE_FAULT;
        }
    }

    /** TODO: doc */
    public LowpanIdentity getLowpanIdentity() {
        LowpanIdentity.Builder builder = new LowpanIdentity.Builder();
        try {
            builder.updateFromMap(getProperties(NETWORK_PROPERTY_KEYS));
        } catch (LowpanException x) {
            // We ignore all LoWPAN-specitic exceptions here.
        }

        return builder.build();
    }

    /**
     * TODO: doc
     *
     * @hide
     */
    public void setLowpanIdentity(LowpanIdentity network) throws LowpanException {
        Map<String, Object> map = new HashMap();
        LowpanIdentity.addToMap(map, network);
        setProperties(map);
    }

    /** TODO: doc */
    @NonNull
    public String getRole() {
        String role = null;

        try {
            role = getProperty(LowpanProperties.KEY_NETWORK_ROLE);
        } catch (LowpanException x) {
            // We ignore all LoWPAN-specitic exceptions here.
            Log.e(TAG, x.toString());
        }

        if (role == null) {
            role = ROLE_DETACHED;
        }

        return role;
    }

    /** TODO: doc */
    @Nullable
    public LowpanCredential getLowpanCredential() {
        LowpanCredential credential = null;

        try {
            Integer keyIndex = getProperty(LowpanProperties.KEY_NETWORK_MASTER_KEY_INDEX);

            if (keyIndex == null) {
                credential =
                        LowpanCredential.createMasterKey(
                                getProperty(LowpanProperties.KEY_NETWORK_MASTER_KEY));
            } else {
                credential =
                        LowpanCredential.createMasterKey(
                                getProperty(LowpanProperties.KEY_NETWORK_MASTER_KEY),
                                keyIndex.intValue());
            }
        } catch (LowpanException x) {
            // We ignore all LoWPAN-specitic exceptions here.
            Log.e(TAG, x.toString());
        }

        return credential;
    }

    /**
     * TODO: doc
     *
     * @hide
     */
    public void setLowpanCredential(LowpanCredential networkCredential) throws LowpanException {
        Map<String, Object> map = new HashMap();
        networkCredential.addToMap(map);
        setProperties(map);
    }

    //////////////////////////////////////////////////////////////////////////
    // Listener Support

    /**
     * Registers a subclass of {@link LowpanInterface.Callback} to receive events.
     *
     * @param cb Subclass of {@link LowpanInterface.Callback} which will receive events.
     * @param handler If not <code>null</code>, events will be dispatched via the given handler
     *     object. If <code>null</code>, the thread upon which events will be dispatched is
     *     unspecified.
     * @see #registerCallback(Callback)
     * @see #unregisterCallback(Callback)
     */
    public void registerCallback(@NonNull Callback cb, @Nullable Handler handler) {
        ILowpanInterfaceListener.Stub listenerBinder =
                new ILowpanInterfaceListener.Stub() {
                    public void onPropertiesChanged(Map<String, Object> properties) {
                        Runnable runnable =
                                new Runnable() {
                                    @Override
                                    public void run() {
                                        for (String key : (Set<String>) properties.keySet()) {
                                            Object value = properties.get(key);
                                            switch (key) {
                                                case ILowpanInterface.KEY_INTERFACE_ENABLED:
                                                    cb.onEnabledChanged(
                                                            ((Boolean) value).booleanValue());
                                                    break;
                                                case ILowpanInterface.KEY_INTERFACE_UP:
                                                    cb.onUpChanged(
                                                            ((Boolean) value).booleanValue());
                                                    break;
                                                case ILowpanInterface.KEY_INTERFACE_CONNECTED:
                                                    cb.onConnectedChanged(
                                                            ((Boolean) value).booleanValue());
                                                    break;
                                                case ILowpanInterface.KEY_INTERFACE_STATE:
                                                    cb.onStateChanged((String) value);
                                                    break;
                                                case ILowpanInterface.KEY_NETWORK_NAME:
                                                case ILowpanInterface.KEY_NETWORK_PANID:
                                                case ILowpanInterface.KEY_NETWORK_XPANID:
                                                case ILowpanInterface.KEY_CHANNEL:
                                                    cb.onLowpanIdentityChanged(getLowpanIdentity());
                                                    break;
                                                case ILowpanInterface.KEY_NETWORK_ROLE:
                                                    cb.onRoleChanged(value.toString());
                                                    break;
                                            }
                                        }
                                        cb.onPropertiesChanged(properties);
                                    }
                                };

                        if (handler != null) {
                            handler.post(runnable);
                        } else {
                            runnable.run();
                        }
                    }
                };
        try {
            mBinder.addListener(listenerBinder);
        } catch (RemoteException x) {
            // Log and ignore. If this happens, this interface
            // is likely dead anyway.
            Log.e(TAG, x.toString());
        }
        synchronized (mListenerMap) {
            mListenerMap.put(System.identityHashCode(cb), listenerBinder);
        }
    }

    /**
     * Registers a subclass of {@link LowpanInterface.Callback} to receive events.
     *
     * <p>The thread upon which events will be dispatched is unspecified.
     *
     * @param cb Subclass of {@link LowpanInterface.Callback} which will receive events.
     * @see #registerCallback(Callback, Handler)
     * @see #unregisterCallback(Callback)
     */
    public void registerCallback(Callback cb) {
        registerCallback(cb, null);
    }

    /**
     * Unregisters a previously registered callback class.
     *
     * @param cb Subclass of {@link LowpanInterface.Callback} which was previously registered to
     *     receive events.
     * @see #registerCallback(Callback, Handler)
     * @see #registerCallback(Callback)
     */
    public void unregisterCallback(Callback cb) {
        int hashCode = System.identityHashCode(cb);
        ILowpanInterfaceListener listenerBinder = mListenerMap.get(hashCode);

        if (listenerBinder != null) {
            synchronized (mListenerMap) {
                mListenerMap.remove(hashCode);
            }
            try {
                mBinder.removeListener(listenerBinder);
            } catch (RemoteException x) {
                // Catch and ignore all binder exceptions
                Log.e(TAG, x.toString());
            }
        }
    }

  //////////////////////////////////////////////////////////////////////////
  // Active and Passive Scanning

  /**
   * Creates a new {@link android.net.lowpan.LowpanScanner} object for this interface.
   *
   * <p>This method allocates a new unique object for each call.
   *
   * @see android.net.lowpan.LowpanScanner
   */
  public @NonNull LowpanScanner createScanner() {
        return new LowpanScanner(mBinder);
    }

    //////////////////////////////////////////////////////////////////////////
    // Route Management

    /**
     * Advertise the given IP prefix as an on-mesh prefix.
     *
     * @hide
     */
    public void addOnMeshPrefix(IpPrefix prefix, int flags) throws LowpanException {
        try {
            mBinder.addOnMeshPrefix(prefix, flags);
        } catch (RemoteException x) {
            throwAsPublicException(x);
        } catch (ServiceSpecificException x) {
            throwAsPublicException(x);
        }
    }

    /**
     * Remove an IP prefix previously advertised by this device from the list of advertised on-mesh
     * prefixes.
     *
     * @hide
     */
    public void removeOnMeshPrefix(IpPrefix prefix) {
        try {
            mBinder.removeOnMeshPrefix(prefix);
        } catch (RemoteException x) {
            // Catch and ignore all binder exceptions
            Log.e(TAG, x.toString());
        } catch (ServiceSpecificException x) {
            // Catch and ignore all service exceptions
            Log.e(TAG, x.toString());
        }
    }

    /**
     * Advertise this device to other devices on the mesh network as having a specific route to the
     * given network. This device will then receive forwarded traffic for that network.
     *
     * @hide
     */
    public void addExternalRoute(IpPrefix prefix, int flags) throws LowpanException {
        try {
            mBinder.addExternalRoute(prefix, flags);
        } catch (RemoteException x) {
            throwAsPublicException(x);
        } catch (ServiceSpecificException x) {
            throwAsPublicException(x);
        }
    }

    /**
     * Revoke a previously advertised specific route to the given network.
     *
     * @hide
     */
    public void removeExternalRoute(IpPrefix prefix) {
        try {
            mBinder.removeExternalRoute(prefix);
        } catch (RemoteException x) {
            // Catch and ignore all binder exceptions
            Log.e(TAG, x.toString());
        } catch (ServiceSpecificException x) {
            // Catch and ignore all service exceptions
            Log.e(TAG, x.toString());
        }
    }
}
