//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements.  See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership.  The ASF licenses this file
// to you 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 com.chirpsdk.rctchirpsdk;

import java.util.Map;
import java.util.HashMap;
import java.util.Random;
import java.io.IOException;

import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.WritableArray;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.LifecycleEventListener;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.modules.core.DeviceEventManagerModule;

import io.chirp.chirpsdk.ChirpSDK;
import io.chirp.chirpsdk.interfaces.ChirpEventListener;
import io.chirp.chirpsdk.interfaces.ChirpSetConfigListener;
import io.chirp.chirpsdk.models.ChirpError;
import io.chirp.chirpsdk.models.ChirpSDKState;


public class RCTChirpSDKModule extends ReactContextBaseJavaModule implements LifecycleEventListener {

    private static final String TAG = "ChirpSDK";
    private ChirpSDK chirp = null;
    private ReactContext context;
    private boolean isStarted = false;

    @Override
    public String getName() {
        return TAG;
    }

    public RCTChirpSDKModule(ReactApplicationContext reactContext) {
        super(reactContext);
        context = reactContext;
        reactContext.addLifecycleEventListener(this);
    }

    @Override
    public Map<String, Object> getConstants() {
        final Map<String, Object> constants = new HashMap<>();
        constants.put("CHIRP_SDK_NOT_CREATED", ChirpSDKState.CHIRP_SDK_STATE_NOT_CREATED.getCode());
        constants.put("CHIRP_SDK_STATE_STOPPED", ChirpSDKState.CHIRP_SDK_STATE_STOPPED.getCode());
        constants.put("CHIRP_SDK_STATE_RUNNING", ChirpSDKState.CHIRP_SDK_STATE_RUNNING.getCode());
        constants.put("CHIRP_SDK_STATE_SENDING", ChirpSDKState.CHIRP_SDK_STATE_SENDING.getCode());
        constants.put("CHIRP_SDK_STATE_RECEIVING", ChirpSDKState.CHIRP_SDK_STATE_RECEIVING.getCode());
        return constants;
    }

    /**
     * init(key, secret)
     *
     * Initialise the SDK with an application key and secret.
     * Callbacks are also set up here.
     */
    @ReactMethod
    public void init(String key, String secret) {
        chirp = new ChirpSDK(this.getCurrentActivity(), key, secret);

        chirp.setListener(new ChirpEventListener() {

            @Override
            public void onSending(byte[] data, int channel) {
                WritableMap params = assembleData(data);
                context.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).emit("onSending", params);
            }

            @Override
            public void onSent(byte[] data, int channel) {
                WritableMap params = assembleData(data);
                context.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).emit("onSent", params);
            }

            @Override
            public void onReceiving(int channel) {
                WritableMap params = Arguments.createMap();
                context.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).emit("onReceiving", params);
            }

            @Override
            public void onReceived(byte[] data, int channel) {
                WritableMap params = assembleData(data);
                context.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).emit("onReceived", params);
            }

            @Override
            public void onStateChanged(int oldState, int newState) {
                WritableMap params = Arguments.createMap();
                params.putInt("status", newState);
                context.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).emit("onStateChanged", params);
            }

            @Override
            public void onSystemVolumeChanged(float oldVolume, float newVolume) {
            }
        });
    }

    /**
     * setConfigFromNetwork()
     *
     * Fetch default config from the network to configure the SDK.
     */
    @ReactMethod
    public void setConfigFromNetwork(final Promise promise) {
        chirp.setConfigFromNetwork(new ChirpSetConfigListener() {

            @Override
            public void onSuccess() {
                promise.resolve("Initialisation Success");
            }

            @Override
            public void onError(ChirpError setConfigError) {
                promise.reject("Network Error", setConfigError.getMessage());
            }
        });
    }

    /**
     * setConfig(config)
     *
     * Configure the SDK with a config string.
     */
    @ReactMethod
    public void setConfig(String config) {

        ChirpError error = chirp.setConfig(config);
        if (error.getCode() > 0) {
            onError(context, error.getMessage());
        }
    }

    /**
     * start()
     *
     * Starts the SDK.
     */
    @ReactMethod
    public void start() {
        ChirpError error = chirp.start();
        if (error.getCode() > 0) {
            onError(context, error.getMessage());
        } else {
            isStarted = true;
        }
    }

    /**
     * stop()
     *
     * Stops the SDK.
     */
    @ReactMethod
    public void stop() {
        ChirpError error = chirp.stop();
        if (error.getCode() > 0) {
            onError(context, error.getMessage());
        } else {
            isStarted = false;
        }
    }

    /**
     * send(data)
     *
     * Encodes a payload of bytes, and sends to the speaker.
     */
    @ReactMethod
    public void send(ReadableArray data) {
        byte[] payload = new byte[data.size()];
        for (int i = 0; i < data.size(); i++) {
            payload[i] = (byte)data.getInt(i);
        }

        long maxSize = chirp.maxPayloadLength();
        if (maxSize < payload.length) {
            onError(context, "Invalid payload");
            return;
        }
        ChirpError error = chirp.send(payload);
        if (error.getCode() > 0) {
            onError(context, error.getMessage());
        }
    }

    /**
     * sendRandom()
     *
     * Sends a random payload to the speaker.
     */
    @ReactMethod
    public void sendRandom() {
        long maxPayloadLength = chirp.maxPayloadLength();
        long size = (long) new Random().nextInt((int) maxPayloadLength) + 1;
        byte[] payload = chirp.randomPayload((byte)size);

        ChirpError error = chirp.send(payload);
        if (error.getCode() > 0) {
            onError(context, error.getMessage());
        }
    }

    /**
     * asString(data)
     *
     * Returns a payload represented as a hexadecimal string.
     */
    public static String asString(byte[] in) {
        final StringBuilder builder = new StringBuilder();
        for(byte b : in) {
            builder.append(String.format("%02x", b));
        }
        return builder.toString();
    }

    private static WritableMap assembleData(byte[] data) {
        WritableArray payload = Arguments.createArray();
        if (data != null) {
            for (int i = 0; i < data.length; i++) {
                payload.pushInt(data[i]);
            }
        }
        WritableMap params = Arguments.createMap();
        params.putArray("data", payload);
        return params;
    }

    private void onError(ReactContext reactContext,
                         String error) {
        WritableMap params = Arguments.createMap();
        params.putString("message", error);
        reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).emit("onError", params);
    }

    @Override
    public void onHostResume() {
        if (chirp != null && isStarted) {
            chirp.start();
        }
    }

    @Override
    public void onHostPause() {
        if (chirp != null) {
            chirp.stop();
        }
    }

    @Override
    public void onHostDestroy() {
        if (chirp != null) {
            chirp.close();
        }
    }
}