package com.prayagad.speech.utils;

import android.content.Context;
import android.media.AudioFormat;
import android.media.AudioManager;
import android.media.AudioRecord;
import android.media.MediaRecorder;
import android.support.annotation.NonNull;

import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.WritableMap;
import com.google.auth.oauth2.GoogleCredentials;
import com.google.cloud.speech.v1beta1.RecognitionConfig;
import com.google.cloud.speech.v1beta1.SpeechGrpc;
import com.google.cloud.speech.v1beta1.StreamingRecognitionConfig;
import com.google.cloud.speech.v1beta1.StreamingRecognizeRequest;
import com.google.cloud.speech.v1beta1.StreamingRecognizeResponse;

import org.apache.log4j.Logger;

import java.io.InputStream;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;

import io.grpc.ManagedChannel;
import io.grpc.ManagedChannelBuilder;
import io.grpc.auth.MoreCallCredentials;
import io.grpc.stub.StreamObserver;

/**
 * Created by skishore on 3/1/17.
 */

public class RNGoogleSpeechWrapper extends ReactContextBaseJavaModule {


    public static final int MULTIPLY = 2;
    //Defined as rootPath/recordings/fileName.pem
    private static final String FILE_LOCATION = "%s/%s.pcm";
    private static final Collection<String> OAUTH2_SCOPES = Arrays.asList("https://www.googleapis.com/auth/cloud-platform");
    public static final int N_THREADS = 5;
    public static final String SPEECH_GOOGLEAPIS = "speech.googleapis.com";
    public static final int GOOGLE_SPEECH_PORT = 443;
    public static final int TIMEOUT = 5;
    private SpeechGrpc.SpeechStub speechClient;
    private GoogleCredentials creds;
    private ManagedChannel channel;
    private AtomicBoolean isRecording = new AtomicBoolean(false);
    private static boolean streamingStatus = false;
    private ExecutorService executorService;
    private AudioRecordProxy audioRecordProxy;
    private static final Logger logger = Logger.getLogger(RNGoogleSpeechWrapper.class);

    private StreamObserver<StreamingRecognizeRequest> requestObserver;
    private Future<Integer> threadReference;
    private StreamObserver<StreamingRecognizeResponse> responseObserver;
    private Boolean saveToFile;
    private String languageCode;
    private Boolean showInterimResults;
    private String fileName = null;
    private StreamingRecognitionConfig streamingConfig;

    @Override
    public String getName() {
        return "ReactNativeGoogleCloudSpeech";
    }

    public RNGoogleSpeechWrapper(ReactApplicationContext reactApplicationContext) {
        super(reactApplicationContext);
    }

    @Override
    public Map<String, Object> getConstants() {
        final Map<String, Object> constants = new HashMap<String, Object>();

        return constants;
    }

    @ReactMethod
    public void init(String authRequestFile, Boolean saveToFile, Boolean showInterimResults, String languageCode, Promise promise) {
        try {
            //Initializing the Speech Stubs
            InputStream authInputStream = getReactApplicationContext().getAssets().open(authRequestFile);
            creds = GoogleCredentials.fromStream(authInputStream);
            creds.createScoped(OAUTH2_SCOPES);

            executorService = Executors.newFixedThreadPool(N_THREADS);
            this.saveToFile = saveToFile;
            this.languageCode = languageCode;
            this.showInterimResults = showInterimResults;
            //Initializing the AudioRecorder
            audioRecordProxy = new AudioRecordProxy(this.getContext());
            audioRecordProxy.start();
            responseObserver = new GoogleStreamResponseListener(this);
            RecognitionConfig config = RecognitionConfig.newBuilder()
                    .setEncoding(RecognitionConfig.AudioEncoding.LINEAR16)
                    .setSampleRate(audioRecordProxy.getRecorderSamplerRate())
                    .setMaxAlternatives(30)
                    .setLanguageCode(languageCode)
                    .build();
            streamingConfig = StreamingRecognitionConfig.newBuilder()
                    .setConfig(config)
                    .setInterimResults(showInterimResults)
                    .setSingleUtterance(false)
                    .build();
            StreamingRecognizeRequest initial =
                    StreamingRecognizeRequest.newBuilder().setStreamingConfig(streamingConfig).build();

            System.out.println("ALL WORKED WELL IN INIT()");

            WritableMap response = Arguments.createMap();
            response.putString("status", "RECORDER INITIALIZED");
            promise.resolve(response);
        } catch (Exception e) {
            promise.reject("INIT FAILED", e.getMessage());
            System.out.println("ERROR CALL FROM INIT");
            System.out.println(e.getMessage());
            e.printStackTrace();
            ;
        }

    }

    private ManagedChannel initChannel() {
        if (channel == null || channel.isShutdown() || channel.isTerminated()) {
            channel = createChannel(SPEECH_GOOGLEAPIS, GOOGLE_SPEECH_PORT);
            return channel;
        }
        return this.channel;
    }


    private ManagedChannel createChannel(String host, int port) {
        return ManagedChannelBuilder.forAddress(host, port).build();
    }


    @ReactMethod
    public void startRecording(Promise promise) {

        /*
        1. Establising the connection with GoogleSpeech
        2. Start Recording
        3. Streaming Data
        */
        try {

            if (isRecording.get()) {
                promise.reject("ERROR STARTING", "ALREADY RUNNING");
                return;
            }
            WritableMap response = startRecording();
            promise.resolve(response);
        } catch (Exception e) {
            System.out.println(e.getMessage());
            promise.reject("ERROR_RECORDING", e.getStackTrace().toString());
            if (audioRecordProxy.getAudioRecordInstance() != null && audioRecordProxy.getAudioRecordInstance().getState() == AudioRecord.STATE_INITIALIZED && audioRecordProxy.getAudioRecordInstance().getRecordingState() == AudioRecord.RECORDSTATE_RECORDING) {
                audioRecordProxy.getAudioRecordInstance().stop();
            }
            isRecording.set(false);
            streamingStatus = false;
            e.printStackTrace();
        }
    }

    @NonNull
    public WritableMap startRecording() {
        //Changing the state
        isRecording.set(true);
        streamingStatus = true;

        //Init Connection
        initChannel();
        speechClient = SpeechGrpc.newStub(channel).withCallCredentials(MoreCallCredentials.from(creds));

        //1st Establishing the connection with GoogleSpeech
        requestObserver = speechClient.streamingRecognize(responseObserver);

        //2nd Start Recording
        System.out.println("ByteBuffer:" + audioRecordProxy.getBytesPerBuffer());
        System.out.println("SamplingRate:" + audioRecordProxy.getRecorderSamplerRate());
        audioRecordProxy.getAudioRecordInstance().startRecording();

        //3rd Streaming data
        // Creating a  new Thread to do this work in background.

        if (saveToFile) {
            fileName = UUID.randomUUID().toString();
            fileName = String.format(FILE_LOCATION, getReactApplicationContext().getFilesDir(), fileName);
            System.out.println("FILE ABSOLURE PATH" + fileName);
        }

        AudioStreamerCallable recordingThreadRunnable = new AudioStreamerCallable(audioRecordProxy.getAudioRecordInstance(), audioRecordProxy.getBytesPerBuffer(), requestObserver, streamingConfig, isRecording, fileName);
        threadReference = executorService.submit(recordingThreadRunnable);

        WritableMap response = Arguments.createMap();
        response.putString("status", "RECORDING STARTED");
        response.putString("filePath", fileName);
        return response;
    }

    // It stops
    // 1. Recording.
    // 2. Streaming.
    @ReactMethod
    public void stopRecording(Promise promise) {

        try {
            WritableMap response = stopRecording();
            promise.resolve(response);

        } catch (Exception e) {
            System.out.println("Exception" + e.getMessage());
            promise.reject("ERROR STOPPING", e.getMessage().toString());
            e.printStackTrace();
        }
    }

    @NonNull
    public WritableMap stopRecording() throws Exception {
        if (!isRecording.get()) {
            throw new Exception("Recording Already Stopped");
        }

        //Stopping the Recorder nad Streamer
        if (audioRecordProxy.getAudioRecordInstance() != null && audioRecordProxy.getAudioRecordInstance().getState() == AudioRecord.STATE_INITIALIZED && audioRecordProxy.getAudioRecordInstance().getRecordingState() == AudioRecord.RECORDSTATE_RECORDING) {
            audioRecordProxy.getAudioRecordInstance().stop();
        }

        //Changing the state
        isRecording.set(false);

        if (!threadReference.isDone()) {
            //Forcing the kill of Thread if the AtomicBoolean did not stop the Thread.
            System.out.println(isRecording.get());
            Integer result;
            try {
                //recordingThread.interrupt();
                result = threadReference.get(TIMEOUT, TimeUnit.SECONDS);
            } catch (TimeoutException e) {
                //The thread is runnning for too long after stopping
                threadReference.cancel(true);
            }

        }

        if (streamingStatus) {
            streamingStatus = false;
        }
        channel.shutdown();

        WritableMap response = Arguments.createMap();
        response.putString("status", "RECORDING STOPPED");
        return response;
    }

    //Destroys the Recorder cleanly.
    @ReactMethod
    public void destroy(Promise promise) {

        //Shutting down all the services
        channel.shutdown();
        executorService.shutdown();
        audioRecordProxy.stop();
        //Clean slating the states.
        streamingStatus = false;
        isRecording.set(false);
        WritableMap response = Arguments.createMap();
        response.putString("status", "RECORDER DESTROYED");
        promise.resolve(response);
    }


    //Hack to get react context
    public ReactApplicationContext getContext() {
        return this.getReactApplicationContext();
    }

}


