// Copyright 2013 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.chromoting.jni;

import android.app.Activity;
import android.app.AlertDialog;
import android.app.ProgressDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.SharedPreferences;
import android.graphics.Bitmap;
import android.graphics.Point;
import android.os.Looper;
import android.text.InputType;
import android.util.Log;
import android.view.KeyEvent;
import android.view.View;
import android.view.inputmethod.EditorInfo;
import android.widget.CheckBox;
import android.widget.TextView;
import android.widget.Toast;

import org.chromium.chromoting.R;

import java.nio.ByteBuffer;
import java.nio.ByteOrder;

/**
 * Initializes the Chromium remoting library, and provides JNI calls into it.
 * All interaction with the native code is centralized in this class.
 */
public class JniInterface {
    /** The status code indicating successful connection. */
    private static final int SUCCESSFUL_CONNECTION = 3;

    /** The application context. */
    private static Activity sContext = null;

    /*
     * Library-loading state machine.
     */
    /** Whether we've already loaded the library. */
    private static boolean sLoaded = false;

    /**
     * To be called once from the main Activity. Any subsequent calls will update the application
     * context, but not reload the library. This is useful e.g. when the activity is closed and the
     * user later wants to return to the application.
     */
    public static void loadLibrary(Activity context) {
        sContext = context;

        synchronized(JniInterface.class) {
            if (sLoaded) return;
        }

        System.loadLibrary("remoting_client_jni");
        loadNative(context);
        sLoaded = true;
    }

    /** Performs the native portion of the initialization. */
    private static native void loadNative(Context context);

    /*
     * API/OAuth2 keys access.
     */
    public static native String getApiKey();
    public static native String getClientId();
    public static native String getClientSecret();

    /*
     * Connection-initiating state machine.
     */
    /** Whether the native code is attempting a connection. */
    private static boolean sConnected = false;

    /** Callback to signal upon successful connection. */
    private static Runnable sSuccessCallback = null;

    /** Dialog for reporting connection progress. */
    private static ProgressDialog sProgressIndicator = null;

    /** Attempts to form a connection to the user-selected host. */
    public static void connectToHost(String username, String authToken,
            String hostJid, String hostId, String hostPubkey, Runnable successCallback) {
        synchronized(JniInterface.class) {
            if (!sLoaded) return;

            if (sConnected) {
                disconnectFromHost();
            }
        }

        sSuccessCallback = successCallback;
        SharedPreferences prefs = sContext.getPreferences(Activity.MODE_PRIVATE);
        connectNative(username, authToken, hostJid, hostId, hostPubkey,
                prefs.getString(hostId + "_id", ""), prefs.getString(hostId + "_secret", ""));
        sConnected = true;
    }

    /** Severs the connection and cleans up. */
    public static void disconnectFromHost() {
        synchronized(JniInterface.class) {
            if (!sLoaded || !sConnected) return;

            if (sProgressIndicator != null) {
                sProgressIndicator.dismiss();
                sProgressIndicator = null;
            }
        }

        disconnectNative();
        sSuccessCallback = null;
        sConnected = false;
    }

    /** Performs the native portion of the connection. */
    private static native void connectNative(String username, String authToken, String hostJid,
            String hostId, String hostPubkey, String pairId, String pairSecret);

    /** Performs the native portion of the cleanup. */
    private static native void disconnectNative();

    /** Position of cursor hotspot within cursor image. */
    public static Point getCursorHotspot() { return sCursorHotspot; }

    /** Returns the current cursor shape. */
    public static Bitmap getCursorBitmap() { return sCursorBitmap; }

    /*
     * Entry points *from* the native code.
     */
    /** Callback to signal whenever we need to redraw. */
    private static Runnable sRedrawCallback = null;

    /** Screen width of the video feed. */
    private static int sWidth = 0;

    /** Screen height of the video feed. */
    private static int sHeight = 0;

    /** Buffer holding the video feed. */
    private static ByteBuffer sBuffer = null;

    /** Position of cursor hot-spot. */
    private static Point sCursorHotspot = new Point();

    /** Bitmap holding the cursor shape. */
    private static Bitmap sCursorBitmap = null;

    /** Reports whenever the connection status changes. */
    private static void reportConnectionStatus(int state, int error) {
        if (state < SUCCESSFUL_CONNECTION && error == 0) {
            // The connection is still being established, so we'll report the current progress.
            synchronized (JniInterface.class) {
                if (sProgressIndicator == null) {
                    sProgressIndicator = ProgressDialog.show(sContext, sContext.
                            getString(R.string.progress_title), sContext.getResources().
                            getStringArray(R.array.protoc_states)[state], true, true,
                            new DialogInterface.OnCancelListener() {
                                @Override
                                public void onCancel(DialogInterface dialog) {
                                    Log.i("jniiface", "User canceled connection initiation");
                                    disconnectFromHost();
                                }
                            });
                }
                else {
                    sProgressIndicator.setMessage(
                            sContext.getResources().getStringArray(R.array.protoc_states)[state]);
                }
            }
        }
        else {
            // The connection is complete or has failed, so we can lose the progress indicator.
            synchronized (JniInterface.class) {
                if (sProgressIndicator != null) {
                    sProgressIndicator.dismiss();
                    sProgressIndicator = null;
                }
            }

            if (state == SUCCESSFUL_CONNECTION) {
                Toast.makeText(sContext, sContext.getResources().
                        getStringArray(R.array.protoc_states)[state], Toast.LENGTH_SHORT).show();

                // Actually display the remote desktop.
                sSuccessCallback.run();
            } else {
                Toast.makeText(sContext, sContext.getResources().getStringArray(
                        R.array.protoc_states)[state] + (error == 0 ? "" : ": " +
                        sContext.getResources().getStringArray(R.array.protoc_errors)[error]),
                        Toast.LENGTH_LONG).show();
            }
        }
    }

    /** Prompts the user to enter a PIN. */
    private static void displayAuthenticationPrompt(boolean pairingSupported) {
        AlertDialog.Builder pinPrompt = new AlertDialog.Builder(sContext);
        pinPrompt.setTitle(sContext.getString(R.string.pin_entry_title));
        pinPrompt.setMessage(sContext.getString(R.string.pin_entry_message));
        pinPrompt.setIcon(android.R.drawable.ic_lock_lock);

        final View pinEntry = sContext.getLayoutInflater().inflate(R.layout.pin_dialog, null);
        pinPrompt.setView(pinEntry);

        final TextView pinTextView = (TextView)pinEntry.findViewById(R.id.pin_dialog_text);
        final CheckBox pinCheckBox = (CheckBox)pinEntry.findViewById(R.id.pin_dialog_check);

        if (!pairingSupported) {
            pinCheckBox.setChecked(false);
            pinCheckBox.setVisibility(View.GONE);
        }

        pinPrompt.setPositiveButton(
                R.string.pin_entry_connect, new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        Log.i("jniiface", "User provided a PIN code");
                        authenticationResponse(String.valueOf(pinTextView.getText()),
                                               pinCheckBox.isChecked());
                    }
                });

        pinPrompt.setNegativeButton(
                R.string.pin_entry_cancel, new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        Log.i("jniiface", "User canceled pin entry prompt");
                        Toast.makeText(sContext,
                                sContext.getString(R.string.msg_pin_canceled),
                                Toast.LENGTH_LONG).show();
                        disconnectFromHost();
                    }
                });

        final AlertDialog pinDialog = pinPrompt.create();

        pinTextView.setOnEditorActionListener(
                new TextView.OnEditorActionListener() {
                    @Override
                    public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
                        // The user pressed enter on the keypad (equivalent to the connect button).
                        pinDialog.getButton(AlertDialog.BUTTON_POSITIVE).performClick();
                        pinDialog.dismiss();
                        return true;
                    }
                });

        pinDialog.setOnCancelListener(
                new DialogInterface.OnCancelListener() {
                    @Override
                    public void onCancel(DialogInterface dialog) {
                        // The user backed out of the dialog (equivalent to the cancel button).
                        pinDialog.getButton(AlertDialog.BUTTON_NEGATIVE).performClick();
                    }
                });

        pinDialog.show();
    }

    /** Saves newly-received pairing credentials to permanent storage. */
    private static void commitPairingCredentials(String host, byte[] id, byte[] secret) {
        synchronized (sContext) {
            sContext.getPreferences(Activity.MODE_PRIVATE).edit().
                    putString(host + "_id", new String(id)).
                    putString(host + "_secret", new String(secret)).
                    apply();
        }
    }

    /**
     * Sets the redraw callback to the provided functor. Provide a value of null whenever the
     * window is no longer visible so that we don't continue to draw onto it.
     */
    public static void provideRedrawCallback(Runnable redrawCallback) {
        sRedrawCallback = redrawCallback;
    }

    /** Forces the native graphics thread to redraw to the canvas. */
    public static boolean redrawGraphics() {
        synchronized(JniInterface.class) {
            if (!sConnected || sRedrawCallback == null) return false;
        }

        scheduleRedrawNative();
        return true;
    }

    /** Performs the redrawing callback. This is a no-op if the window isn't visible. */
    private static void redrawGraphicsInternal() {
        if (sRedrawCallback != null)
            sRedrawCallback.run();
    }

    /**
     * Obtains the image buffer.
     * This should not be called from the UI thread. (We prefer the native graphics thread.)
     */
    public static Bitmap retrieveVideoFrame() {
        if (Looper.myLooper() == Looper.getMainLooper()) {
            Log.w("jniiface", "Canvas being redrawn on UI thread");
        }

        if (!sConnected) {
            return null;
        }

        int[] frame = new int[sWidth * sHeight];

        sBuffer.order(ByteOrder.LITTLE_ENDIAN);
        sBuffer.asIntBuffer().get(frame, 0, frame.length);

        return Bitmap.createBitmap(frame, 0, sWidth, sWidth, sHeight, Bitmap.Config.ARGB_8888);
    }

    /**
     * Updates the cursor shape. This is called from native code on the graphics thread when
     * receiving a new cursor shape from the host.
     */
    public static void updateCursorShape(int width, int height, int hotspotX, int hotspotY,
                                         ByteBuffer buffer) {
        sCursorHotspot = new Point(hotspotX, hotspotY);

        int[] data = new int[width * height];
        buffer.order(ByteOrder.LITTLE_ENDIAN);
        buffer.asIntBuffer().get(data, 0, data.length);
        sCursorBitmap = Bitmap.createBitmap(data, width, height, Bitmap.Config.ARGB_8888);
    }

    /** Moves the mouse cursor, possibly while clicking the specified (nonnegative) button. */
    public static void mouseAction(int x, int y, int whichButton, boolean buttonDown) {
        if (!sConnected) {
            return;
        }

        mouseActionNative(x, y, whichButton, buttonDown);
    }

    /** Presses and releases the specified (nonnegative) key. */
    public static void keyboardAction(int keyCode, boolean keyDown) {
        if (!sConnected) {
            return;
        }

        keyboardActionNative(keyCode, keyDown);
    }

    /** Performs the native response to the user's PIN. */
    private static native void authenticationResponse(String pin, boolean createPair);

    /** Schedules a redraw on the native graphics thread. */
    private static native void scheduleRedrawNative();

    /** Passes mouse information to the native handling code. */
    private static native void mouseActionNative(int x, int y, int whichButton, boolean buttonDown);

    /** Passes key press information to the native handling code. */
    private static native void keyboardActionNative(int keyCode, boolean keyDown);
}
