/*
 * Copyright 2016 Google LLC.
 * 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 com.google.cloud.solutions.flexenv;

import android.content.Intent;
import android.media.MediaPlayer;
import android.net.Uri;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.design.widget.NavigationView;
import android.support.v4.view.GravityCompat;
import android.support.v4.widget.DrawerLayout;
import android.support.v7.app.ActionBarDrawerToggle;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.util.Log;
import android.view.KeyEvent;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.AdapterView;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ImageButton;
import android.widget.ListView;
import android.widget.SimpleAdapter;
import android.widget.TextView;
import android.widget.Toast;

import com.google.android.gms.auth.api.Auth;
import com.google.android.gms.auth.api.signin.GoogleSignInOptions;
import com.google.android.gms.auth.api.signin.GoogleSignInResult;
import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.SignInButton;
import com.google.android.gms.common.api.GoogleApiClient;
import com.google.android.gms.common.api.Status;
import com.google.cloud.solutions.flexenv.common.Base64EncodingHelper;
import com.google.cloud.solutions.flexenv.common.BaseMessage;
import com.google.cloud.solutions.flexenv.common.GcsDownloadHelper;
import com.google.cloud.solutions.flexenv.common.RecordingHelper;
import com.google.cloud.solutions.flexenv.common.SpeechMessage;
import com.google.cloud.solutions.flexenv.common.SpeechTranslationHelper;
import com.google.cloud.solutions.flexenv.common.TextMessage;
import com.google.cloud.solutions.flexenv.common.Translation;
import com.google.firebase.auth.AuthCredential;
import com.google.firebase.auth.FirebaseAuth;
import com.google.firebase.auth.FirebaseUser;
import com.google.firebase.auth.GoogleAuthProvider;
import com.google.firebase.database.ChildEventListener;
import com.google.firebase.database.DataSnapshot;
import com.google.firebase.database.DatabaseError;
import com.google.firebase.database.DatabaseReference;
import com.google.firebase.database.FirebaseDatabase;
import com.google.firebase.database.ValueEventListener;

import org.chromium.net.CronetEngine;
import org.json.JSONException;
import org.json.JSONObject;

import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;

/*
 * Main activity to select a channel and exchange messages with other users
 * The app expects users to authenticate with Google ID. It also sends user
 * activity logs to a servlet instance through Firebase.
 */
public class PlayActivity
        extends AppCompatActivity
        implements NavigationView.OnNavigationItemSelectedListener,
        GoogleApiClient.OnConnectionFailedListener,
        AdapterView.OnItemClickListener,
        View.OnKeyListener,
        View.OnClickListener {

    // Firebase keys commonly used with backend servlet instances
    private static final String IBX = "inbox";
    private static final String CHS = "channels";
    private static final String REQLOG = "requestLogger";

    private static final int RC_SIGN_IN = 9001;

    private static final String TAG = "PlayActivity";
    private static final String CURRENT_CHANNEL_KEY = "CURRENT_CHANNEL_KEY";
    private static final String INBOX_KEY = "INBOX_KEY";
    private static final String FIREBASE_LOGGER_PATH_KEY = "FIREBASE_LOGGER_PATH_KEY";
    private static FirebaseLogger fbLog;

    private GoogleApiClient mGoogleApiClient;
    private String firebaseLoggerPath;
    private String inbox;
    private String currentChannel;
    private ChildEventListener channelListener;
    private SimpleDateFormat fmt;
    private CronetEngine cronetEngine;

    private Menu channelMenu;
    private TextView channelLabel;
    private List<Map<String, String>> messages;
    private SimpleAdapter messageAdapter;
    private EditText messageText;
    private TextView status;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ListView messageHistory;
        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_play);
        Toolbar toolbar = findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);

        DrawerLayout drawer = findViewById(R.id.drawer_layout);
        ActionBarDrawerToggle toggle = new ActionBarDrawerToggle(
                this, drawer, toolbar, R.string.navigation_drawer_open,
                R.string.navigation_drawer_close);
        drawer.addDrawerListener(toggle);
        toggle.syncState();

        NavigationView navigationView = findViewById(R.id.nav_view);
        channelMenu = navigationView.getMenu();
        navigationView.setNavigationItemSelectedListener(this);
        initChannels();

        GoogleSignInOptions.Builder gsoBuilder =
                new GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
                        .requestIdToken(getString(R.string.default_web_client_id))
                        .requestEmail();

        GoogleSignInOptions gso = gsoBuilder.build();
        mGoogleApiClient = new GoogleApiClient.Builder(this)
                .enableAutoManage(this, this)
                .addApi(Auth.GOOGLE_SIGN_IN_API, gso)
                .build();

        SignInButton signInButton = findViewById(R.id.sign_in_button);
        signInButton.setSize(SignInButton.SIZE_STANDARD);
        signInButton.setOnClickListener(this);
        channelLabel = findViewById(R.id.channelLabel);
        Button signOutButton = findViewById(R.id.sign_out_button);
        signOutButton.setOnClickListener(this);

        ImageButton microphoneButton = findViewById(R.id.microphone_button);
        microphoneButton.setOnClickListener(this);

        messages = new ArrayList<>();
        messageAdapter = new SimpleAdapter(this, messages, android.R.layout.simple_list_item_2,
                new String[]{"message", "meta"},
                new int[]{android.R.id.text1, android.R.id.text2});

        messageHistory = findViewById(R.id.messageHistory);
        messageHistory.setOnItemClickListener(this);
        messageHistory.setAdapter(messageAdapter);
        messageText = findViewById(R.id.messageText);
        messageText.setOnKeyListener(this);
        fmt = new SimpleDateFormat("yy.MM.dd HH:mm z", Locale.US);

        status = findViewById(R.id.status);
    }

    @Override
    public void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if (requestCode == RC_SIGN_IN) {
            GoogleSignInResult result = Auth.GoogleSignInApi.getSignInResultFromIntent(data);
            Log.d(TAG, "Google authentication status: " + result.getStatus().getStatusMessage());
            // If Google ID authentication is successful, obtain a token for Firebase authentication.
            if (result.isSuccess() && result.getSignInAccount() != null) {
                status.setText(getResources().getString(R.string.authenticating_label));
                AuthCredential credential = GoogleAuthProvider.getCredential(
                        result.getSignInAccount().getIdToken(), null);
                FirebaseAuth.getInstance().signInWithCredential(credential)
                        .addOnCompleteListener(this, task -> {
                            Log.d(TAG, "signInWithCredential:onComplete Successful: " + task.isSuccessful());
                            if (task.isSuccessful()) {
                                final FirebaseUser currentUser = FirebaseAuth.getInstance().getCurrentUser();
                                if (currentUser != null) {
                                    inbox = "client-" + Integer.toString(Math.abs(currentUser.getUid().hashCode()));
                                    requestLogger(() -> {
                                        Log.d(TAG, "onLoggerAssigned logger id: " + inbox);
                                        fbLog.log(inbox, "Signed in");
                                        updateUI();
                                    });
                                } else {
                                    updateUI();
                                }
                            } else {
                                Log.w(TAG, "signInWithCredential:onComplete", task.getException());
                                status.setText(String.format(
                                        getResources().getString(R.string.authentication_failed),
                                        task.getException())
                                );
                            }
                        });
            } else if (result.getStatus().isCanceled()) {
                String message = "Google authentication was canceled. "
                        + "Verify the SHA certificate fingerprint in the Firebase console.";
                Log.d(TAG, message);
                showErrorToast(new Exception(message));
            } else {
                Log.d(TAG, "Google authentication status: " + result.getStatus().toString());
                showErrorToast(new Exception(result.getStatus().toString()));
            }
        }
    }

    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.sign_out_button:
                signOut();
                break;
            case R.id.sign_in_button:
                Intent signInIntent = Auth.GoogleSignInApi.getSignInIntent(mGoogleApiClient);
                // Start authenticating with Google ID first.
                startActivityForResult(signInIntent, RC_SIGN_IN);
                break;
            case R.id.microphone_button:
                translateAudioMessage(v);
                break;
        }
    }

    @Override
    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
        if (parent.getAdapter().getItem(position) instanceof Map) {
            Map map = (Map) parent.getAdapter().getItem(position);
            if (map.containsKey("gcsBucket") && map.containsKey("gcsPath")) {
                String gcsBucket = map.get("gcsBucket").toString();
                String gcsPath = map.get("gcsPath").toString();
                playMessage(gcsBucket, gcsPath);
            }
        }
    }

    private void playMessage(String gcsBucket, String gcsPath) {
        String filePath = gcsBucket + "/" + gcsPath;
        File file = new File(getFilesDir(), filePath);

        if(file.exists()) {
            MediaPlayer mediaPlayer = MediaPlayer.create(
                    getApplicationContext(), Uri.fromFile(file));
            mediaPlayer.start();
        } else {
            GcsDownloadHelper.getInstance().downloadGcsFile(
                    getApplicationContext(), getCronetEngine(), gcsBucket, gcsPath,
                    new GcsDownloadHelper.GcsDownloadListener() {
                        @Override
                        public void onDownloadSucceeded(File file) {
                            MediaPlayer mediaPlayer = MediaPlayer.create(
                                    getApplicationContext(), Uri.fromFile(file));
                            mediaPlayer.start();
                        }

                        @Override
                        public void onDownloadFailed(Exception e) {
                            showErrorToast(e);
                            Log.e(TAG, e.getLocalizedMessage());
                        }
                    }
            );
        }
    }

    private void signOut() {
        Auth.GoogleSignInApi.signOut(mGoogleApiClient).setResultCallback(
                status -> {
                    FirebaseAuth.getInstance().signOut();
                    DatabaseReference databaseReference = FirebaseDatabase.getInstance().getReference();
                    databaseReference.removeEventListener(channelListener);
                    databaseReference.onDisconnect();
                    inbox = null;
                    runOnUiThread(PlayActivity.this::updateUI);
                    fbLog.log(inbox, "Signed out");
                });
    }

    @Override
    public boolean onKey(View v, int keyCode, KeyEvent event) {
        if ((event.getAction() == KeyEvent.ACTION_DOWN) && (keyCode == KeyEvent.KEYCODE_ENTER)) {
            FirebaseUser currentUser = FirebaseAuth.getInstance().getCurrentUser();
            if (currentUser != null) {
                FirebaseDatabase.getInstance().getReference().child(CHS + "/" + currentChannel)
                        .push()
                        .setValue(new TextMessage(messageText.getText().toString(), currentUser.getDisplayName(), BaseMessage.MESSAGE_TYPE_TEXT));
                return true;
            } else {
                return false;
            }
        }
        return false;
    }

    private void addMessage(String msgString, String meta) {
        Map<String, String> message = new HashMap<>();
        message.put("message", msgString);
        message.put("meta", meta);
        messages.add(message);

        messageAdapter.notifyDataSetChanged();
        messageText.setText("");
    }
    private void addMessage(String msgString, String meta, String gcsBucket, String gcsPath) {
        Map<String, String> message = new HashMap<>();
        // 🔈 Prepend a speaker emoji to text.
        message.put("message", "\uD83D\uDD08" + msgString);
        message.put("meta", meta);
        message.put("gcsBucket", gcsBucket);
        message.put("gcsPath", gcsPath);
        messages.add(message);

        messageAdapter.notifyDataSetChanged();
    }

    private void translateAudioMessage(View v) {
        ImageButton microphoneButton = (ImageButton)v;
        if (RecordingHelper.getInstance().hasRequiredPermissions(getApplicationContext())) {
            if (!RecordingHelper.getInstance().isRecording()) {
                RecordingHelper.getInstance().startRecording(new RecordingHelper.RecordingListener() {
                    @Override
                    public void onRecordingSucceeded(File output) {
                        String base64EncodedAudioMessage;
                        try {
                            base64EncodedAudioMessage = Base64EncodingHelper.encode(output);
                            SpeechTranslationHelper.getInstance().translateAudioMessage(
                                    getApplicationContext(),
                                    getCronetEngine(),
                                    base64EncodedAudioMessage,
                                    16000,
                                    new SpeechTranslationHelper.SpeechTranslationListener() {
                                        @Override
                                        public void onTranslationSucceeded(String responseBody) {
                                            Log.i(TAG, responseBody);
                                            try {
                                                final FirebaseUser currentUser = FirebaseAuth.getInstance().getCurrentUser();
                                                if(currentUser != null) {
                                                    SpeechMessage speechMessage = new SpeechMessage(
                                                            new JSONObject(responseBody),
                                                            currentUser.getDisplayName(),
                                                            BaseMessage.MESSAGE_TYPE_SPEECH
                                                            );
                                                    FirebaseDatabase.getInstance().getReference().child(CHS + "/" + currentChannel)
                                                            .push()
                                                            .setValue(speechMessage);
                                                }
                                            } catch (JSONException e) {
                                                showErrorToast(e);
                                                Log.e(TAG, e.getLocalizedMessage());
                                            }
                                            microphoneButton.setImageDrawable(getDrawable(R.drawable.ic_baseline_mic_none_24px));
                                        }

                                        @Override
                                        public void onTranslationFailed(Exception e) {
                                            showErrorToast(e);
                                            Log.e(TAG, e.getLocalizedMessage());
                                        }
                                    });
                        } catch (IOException e) {
                            showErrorToast(e);
                            Log.e(TAG, e.getLocalizedMessage());
                        }
                    }

                    @Override
                    public void onRecordingFailed(Exception e) {
                        showErrorToast(e);
                        Log.e(TAG, e.getLocalizedMessage());
                    }
                });
                microphoneButton.setImageDrawable(getDrawable(R.drawable.ic_baseline_mic_24px));
            } else {
                RecordingHelper.getInstance().stopRecording();
            }
        } else {
            RecordingHelper.getInstance().requestRequiredPermissions(this);
        }
    }

    /**
     * Creates an instance of the CronetEngine class.
     * Instances of CronetEngine require a lot of resources. Additionally, their creation is slow
     * and expensive. It's recommended to delay the creation of CronetEngine instances until they
     * are required and reuse them as much as possible.
     * @return An instance of CronetEngine.
     */
    private synchronized CronetEngine getCronetEngine() {
        if(cronetEngine == null) {
            CronetEngine.Builder myBuilder = new CronetEngine.Builder(this);
            cronetEngine = myBuilder.build();
        }
        return cronetEngine;
    }

    private void updateUI() {
        FirebaseUser currentUser = FirebaseAuth.getInstance().getCurrentUser();
        if (currentUser != null) {
            findViewById(R.id.sign_in_button).setVisibility(View.GONE);
            findViewById(R.id.sign_out_button).setVisibility(View.VISIBLE);
            findViewById(R.id.channelLabel).setVisibility(View.VISIBLE);
            findViewById(R.id.messageText).setVisibility(View.VISIBLE);
            findViewById(R.id.messageHistory).setVisibility(View.VISIBLE);

            if(speechTranslationEnabled()) {
                findViewById(R.id.microphone_button).setVisibility(View.VISIBLE);
            }

            status.setText(
                    String.format(getResources().getString(R.string.signed_in_label),
                    currentUser.getDisplayName())
            );
            findViewById(R.id.status).setVisibility(View.VISIBLE);

            // Select the first channel in the array if there's no channel selected
            switchChannel(currentChannel != null ? currentChannel :
                    getResources().getStringArray(R.array.channels)[0]);
        } else {
            findViewById(R.id.sign_in_button).setVisibility(View.VISIBLE);
            findViewById(R.id.sign_out_button).setVisibility(View.GONE);
            findViewById(R.id.channelLabel).setVisibility(View.GONE);
            findViewById(R.id.messageText).setVisibility(View.GONE);
            findViewById(R.id.microphone_button).setVisibility(View.GONE);
            findViewById(R.id.messageHistory).setVisibility(View.GONE);
            findViewById(R.id.status).setVisibility(View.GONE);
            ((TextView)findViewById(R.id.status)).setText("");
        }
    }

    @Override
    public void onBackPressed() {
        DrawerLayout drawer = findViewById(R.id.drawer_layout);
        if (drawer.isDrawerOpen(GravityCompat.START)) {
            drawer.closeDrawer(GravityCompat.START);
        } else {
            super.onBackPressed();
        }
    }

    @Override
    public boolean onNavigationItemSelected(@NonNull MenuItem item) {
        DrawerLayout drawer = findViewById(R.id.drawer_layout);
        drawer.closeDrawer(GravityCompat.START);

        switchChannel(item.toString());

        return true;
    }

    private void switchChannel(String channel) {
        messages.clear();

        String msg = "Switching channel to '" + channel + "'";
        fbLog.log(inbox, msg);

        // Switching a listener to the selected channel.
        DatabaseReference databaseReference = FirebaseDatabase.getInstance().getReference();
        databaseReference.child(CHS + "/" + currentChannel).removeEventListener(channelListener);
        currentChannel = channel;
        databaseReference.child(CHS + "/" + currentChannel).addChildEventListener(channelListener);

        channelLabel.setText(currentChannel);
    }

    @Override
    public void onSaveInstanceState(Bundle outState) {
        outState.putString(CURRENT_CHANNEL_KEY, currentChannel);
        outState.putString(INBOX_KEY, inbox);
        outState.putString(FIREBASE_LOGGER_PATH_KEY, firebaseLoggerPath);
        super.onSaveInstanceState(outState);
    }

    @Override
    protected void onRestoreInstanceState(Bundle savedInstanceState) {
        currentChannel = savedInstanceState.getString(CURRENT_CHANNEL_KEY);
        inbox = savedInstanceState.getString(INBOX_KEY);
        firebaseLoggerPath = savedInstanceState.getString(FIREBASE_LOGGER_PATH_KEY);
        FirebaseUser currentUser = FirebaseAuth.getInstance().getCurrentUser();
        if (currentUser != null) {
            fbLog = new FirebaseLogger(firebaseLoggerPath);
            updateUI();
        }
        super.onRestoreInstanceState(savedInstanceState);
    }

    @Override
    public void onConnectionFailed(@NonNull ConnectionResult connectionResult) {}

// [START request_logger]
    /*
     * Request that a servlet instance be assigned.
     */
    private void requestLogger(final LoggerListener loggerListener) {
        final DatabaseReference databaseReference = FirebaseDatabase.getInstance().getReference();
        databaseReference.child(IBX + "/" + inbox).addListenerForSingleValueEvent(new ValueEventListener() {
            public void onDataChange(@NonNull DataSnapshot snapshot) {
                if (snapshot.exists() && snapshot.getValue(String.class) != null) {
                    firebaseLoggerPath = IBX + "/" + snapshot.getValue(String.class) + "/logs";
                    fbLog = new FirebaseLogger(firebaseLoggerPath);
                    databaseReference.child(IBX + "/" + inbox).removeEventListener(this);
                    loggerListener.onLoggerAssigned();
                }
            }

            public void onCancelled(@NonNull DatabaseError error) {
                Log.e(TAG, error.getDetails());
            }
        });

        databaseReference.child(REQLOG).push().setValue(inbox);
    }
// [END request_logger]

    /*
     * Initialize predefined channels as activity menu.
     * Once a channel is selected, ChildEventListener is attached and
     * waits for messages.
     */
    private void initChannels() {
        String[] channelArray = getResources().getStringArray(R.array.channels);
        Log.d(TAG, "Channels : " + Arrays.toString(channelArray));
        for (String topic : channelArray) {
            channelMenu.add(topic);
        }

        channelListener = new ChildEventListener() {
            @Override
            public void onChildAdded(@NonNull DataSnapshot snapshot, String prevKey) {
                if(snapshot.hasChild("/messageType")) {
                    String messageType = snapshot.child("/messageType").getValue(String.class);
                    if(messageType != null) {
                        // Extract attributes from appropriate message object to display on the screen.
                        if (messageType.equals(BaseMessage.MESSAGE_TYPE_TEXT)) {
                            TextMessage message = snapshot.getValue(TextMessage.class);
                            if(message != null) {
                                addMessage(message.getText(),fmt.format(new Date(message.getTimeLong())) + " "
                                        + message.getDisplayName());
                            }
                        } else if (messageType.equals(BaseMessage.MESSAGE_TYPE_SPEECH)) {
                            SpeechMessage message = snapshot.getValue(SpeechMessage.class);
                            String language = getApplicationContext()
                                    .getResources()
                                    .getConfiguration()
                                    .getLocales()
                                    .get(0).getLanguage();
                            if(message != null) {
                                Translation translation = message.getTranslation(language);
                                addMessage(translation.getText(), fmt.format(new Date(message.getTimeLong())) + " "
                                        + message.getDisplayName(), message.getGcsBucket(), translation.getGcsPath());
                            }
                        }
                    }
                }
            }

            @Override
            public void onCancelled(@NonNull DatabaseError error) {
                Log.e(TAG, error.getDetails());
            }

            @Override
            public void onChildChanged(@NonNull DataSnapshot snapshot, String prevKey) {}

            @Override
            public void onChildRemoved(@NonNull DataSnapshot snapshot) {}

            @Override
            public void onChildMoved(@NonNull DataSnapshot snapshot, String prevKey) {}
        };
    }

    private boolean speechTranslationEnabled() {
        String speechEndpoint = getString(R.string.speechToSpeechEndpoint);
        return !speechEndpoint.contains("YOUR-PROJECT-ID");
    }

    private void showErrorToast(Exception e) {
        runOnUiThread(
                () -> Toast.makeText(
                        getApplicationContext(),
                        e.getLocalizedMessage(),
                        Toast.LENGTH_LONG).show()
        );
    }

    /**
     * A listener to get notifications about server-side loggers.
     */
    private interface LoggerListener {
        /**
         * Called when a logger has been assigned to this client.
         */
        void onLoggerAssigned();
    }
}