/*
 * Copyright 2014 OpenMarket Ltd
 *
 * 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 org.matrix.console.util;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.zip.GZIPOutputStream;

import android.app.AlertDialog;
import android.app.Dialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageInfo;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.net.Uri;
import android.os.Build;
import android.os.Environment;
import android.preference.PreferenceManager;
import android.provider.MediaStore;
import android.util.Log;
import android.view.Gravity;
import android.view.View;
import android.view.WindowManager;

import org.matrix.androidsdk.MXSession;
import org.matrix.console.ConsoleApplication;
import org.matrix.console.Matrix;
import org.matrix.androidsdk.data.MyUser;
import org.matrix.console.R;


public class RageShake implements SensorEventListener {
    private static final String LOG_TAG = "RageShake";

    private static RageShake instance;

    private Context mContext;

    // weak refs so dead dialogs can be GCed
    private List<WeakReference<Dialog>> mDialogs;
    
    protected RageShake() {
        mDialogs = new ArrayList<WeakReference<Dialog>>();

        // Samsung devices for some reason seem to be less sensitive than others so the threshold is being
        // lowered for them. A possible lead for a better formula is the fact that the sensitivity detected 
        // with the calculated force below seems to relate to the sample rate: The higher the sample rate,
        // the higher the sensitivity.
        String model = Build.MODEL.trim();
        // S3, S1(Brazil), Galaxy Pocket
        if ("GT-I9300".equals(model) || "GT-I9000B".equals(model) || "GT-S5300B".equals(model)) {
            threshold = 20.0f;
        }
    }

    public synchronized static RageShake getInstance() {
        if (instance == null) {
            instance = new RageShake();
        }
        return instance;
    }

    public void registerDialog(Dialog d) {
        mDialogs.add(new WeakReference<Dialog>(d));
    }

    public void sendBugReport() {
        Bitmap screenShot = this.takeScreenshot();

        if (null != screenShot) {
            try {
                // store the file in shared place
                String path = MediaStore.Images.Media.insertImage(mContext.getContentResolver(), screenShot, "screenshot-" + new Date(), null);

                Intent intent = new Intent(Intent.ACTION_SEND_MULTIPLE);
                intent.setType("text/html");
                intent.putExtra(Intent.EXTRA_EMAIL, new String[]{"[email protected]"});
                intent.putExtra(Intent.EXTRA_SUBJECT, "Matrix bug report");

                String message = "Something went wrong on my Matrix client : \n\n\n";
                message += "-----> my comments <-----\n\n\n";
                message += "------------------------------\n";

                message += "Application info\n";

                Collection<MXSession> sessions = Matrix.getMXSessions(mContext);
                int profileIndex = 1;

                for(MXSession session : sessions) {
                    message += "Profile " + profileIndex + " :\n";
                    profileIndex++;

                    MyUser mMyUser = session.getMyUser();
                    message += "userId : "+ mMyUser.user_id + "\n";
                    message += "displayname : " + mMyUser.displayname + "\n";
                    message += "homeServer :" + session.getCredentials().homeServer + "\n";
                }

                message += "\n";

                message += "matrixConsole version: " + Matrix.getInstance(mContext).getVersion(true) + "\n";
                message += "SDK version:  " + Matrix.getInstance(mContext).getDefaultSession().getVersion(true) + "\n";

                message += "\n\n\n";

                intent.putExtra(Intent.EXTRA_TEXT, message);

                ArrayList<Uri> attachmentUris = new ArrayList<Uri>();

                // attachments
                intent.setType("image/jpg");
                attachmentUris.add(Uri.parse(path));

                String errorLog = LogUtilities.getLogCatError();
                String debugLog = LogUtilities.getLogCatDebug();

                errorLog += "\n\n\n\n\n\n\n\n\n\n------------------ Debug logs ------------------\n\n\n\n\n\n\n\n";
                errorLog += debugLog;

                try {

                    // add the current device logs
                    {
                        ByteArrayOutputStream os = new ByteArrayOutputStream();
                        GZIPOutputStream gzip = new GZIPOutputStream(os);
                        gzip.write(errorLog.getBytes());
                        gzip.finish();

                        File debugLogFile = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), "logs-" + new Date() + ".gz");
                        FileOutputStream fos = new FileOutputStream(debugLogFile);
                        os.writeTo(fos);
                        os.flush();
                        os.close();

                        attachmentUris.add(Uri.fromFile(debugLogFile));
                    }

                    // add the stored logs
                    ArrayList<File> logsList = LogUtilities.getLogsFileList();

                    long marker = System.currentTimeMillis();

                    for(File file : logsList) {
                        ByteArrayOutputStream bos = new ByteArrayOutputStream();
                        GZIPOutputStream glogzip = new GZIPOutputStream(bos);

                        FileInputStream inputStream = new FileInputStream(file);

                        byte[] buffer = new byte[1024 * 10];
                        int len;
                        while ((len = inputStream.read(buffer)) != -1) {
                            glogzip.write(buffer, 0, len);
                        }
                        glogzip.finish();

                        File storedLogFile = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), marker + "-" + file.getName() + ".gz");
                        FileOutputStream flogOs = new FileOutputStream(storedLogFile);
                        bos.writeTo(flogOs);
                        flogOs.flush();
                        flogOs.close();

                        attachmentUris.add(Uri.fromFile(storedLogFile));
                    }
                }
                catch (IOException e) {
                    Log.e(LOG_TAG, "" + e);
                }

                intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, attachmentUris);

                ConsoleApplication.getCurrentActivity().startActivity(intent);
            } catch (Exception e) {
            }
        }
    }

    public void promptForReport() {
        // Cannot prompt for bug, no active activity.
        if (ConsoleApplication.getCurrentActivity() == null) {
            return;
        }

        // The user is trying to leave with unsaved changes. Warn about that
        new AlertDialog.Builder(ConsoleApplication.getCurrentActivity())
                .setMessage("You seem to be shaking the phone in frustration. Would you like to submit a bug report?")
                .setPositiveButton("YES", new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        dialog.dismiss();
                        sendBugReport();
                    }
                })
                .setNeutralButton("Disable", new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(mContext);

                        SharedPreferences.Editor editor = preferences.edit();
                        editor.putBoolean(mContext.getString(R.string.settings_key_use_rage_shake), false);
                        editor.commit();

                        dialog.dismiss();
                    }
                })
                .setNegativeButton("NO", new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        dialog.dismiss();
                    }
                })
                .create()
                .show();

    }

    private Bitmap takeScreenshot() {
        if (ConsoleApplication.getCurrentActivity() == null) {
            return null;
        }
        
        // get content view
        View contentView = ConsoleApplication.getCurrentActivity().findViewById(android.R.id.content);
        if (contentView == null) {
            Log.e(LOG_TAG, "Cannot find content view on " + ConsoleApplication.getCurrentActivity() + ". Cannot take screenshot.");
            return null;
        }
        
        // get the root view to snapshot
        View rootView = contentView.getRootView();
        if (rootView == null) {
            Log.e(LOG_TAG, "Cannot find root view on " + ConsoleApplication.getCurrentActivity() + ". Cannot take screenshot.");
            return null;
        }
        // refresh it
        rootView.setDrawingCacheEnabled(false);
        rootView.setDrawingCacheEnabled(true);
        
        try {
            Bitmap baseScreen = rootView.getDrawingCache();
            
            // loop the dialogs and prune old/not visible ones
            List<Dialog> onScreenDialogs = new ArrayList<Dialog>();
            for (int i=0; i<mDialogs.size(); i++) {
                WeakReference<Dialog> wfd = mDialogs.get(i);
                Dialog d = wfd.get();
                if (d == null || (d != null && !d.isShowing())) {
                    Log.d(LOG_TAG,  "Discarding empty/null dialog. "+d);
                    mDialogs.remove(i);
                    i--;
                    continue;
                }
                onScreenDialogs.add(d);
            }
        
            if (onScreenDialogs.size() == 0) {
                Log.d(LOG_TAG, "No on screen dialogs.");
                return baseScreen;
            }
            else {
                // use a canvas to draw on top of the base screen.
                Canvas c = new Canvas(baseScreen);
                for (Dialog d : onScreenDialogs) {
                    if (d.getWindow() != null && d.getWindow().getAttributes() != null) {
                        View dialogView = d.getWindow().peekDecorView();
                        Bitmap dialogBitmap = null;
                        // get the dialog bitmap
                        if (dialogView != null) {
                            dialogView.setDrawingCacheEnabled(false);
                            dialogView.setDrawingCacheEnabled(true);
                            dialogBitmap = dialogView.getDrawingCache();
                        }
                        if (dialogBitmap == null) {
                            Log.w(LOG_TAG, "Cannot get dialog bitmap.");
                            continue;
                        }
                        
                        // draw it to the canvas in the right place
                        WindowManager.LayoutParams params = d.getWindow().getAttributes();
                        int x = params.x;
                        int y = params.y;
                        int w = dialogView.getWidth();
                        int h = dialogView.getHeight();
                        int gravity = params.gravity;
                        Log.d(LOG_TAG, "Dialog x "+x+" y "+y+" w "+w+" h "+h+" gravity "+gravity);
                        if (x == 0 && y == 0 && w < baseScreen.getWidth() && h < baseScreen.getHeight()) {
                            switch (gravity) {
                            case Gravity.CENTER:
                                // mid-point - 1/2
                                x = baseScreen.getWidth()/2 - (w/2);
                                y = baseScreen.getHeight()/2 - (h/2);
                                break;
                            default:
                                Log.w(LOG_TAG, "Unhandled gravity: "+gravity);
                                break;
                            }
                        }
                        c.drawBitmap(dialogBitmap, x, y, null);
                        Log.d(LOG_TAG, "Drew a dialog to the canvas");
                    }
                }
                return baseScreen;
            }
        }
        catch (OutOfMemoryError oom) {
            Log.e(LOG_TAG, "Cannot get drawing cache for "+ ConsoleApplication.getCurrentActivity() +" OOM.");
        }
        catch (Exception e) {
            Log.e(LOG_TAG, "Cannot get snapshot of screen: "+e);
        }
        return null;
    }

    /**
     * start the sensor detector
     */
    public void start(Context context) {
        mContext = context;

        SensorManager sm = (SensorManager)context.getSystemService(Context.SENSOR_SERVICE);
        Sensor s = sm.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
        if (s == null) {
            Log.e(LOG_TAG, "No accelerometer in this device. Cannot use rage shake.");
            return;
        }
        sm.registerListener(this, s, SensorManager.SENSOR_DELAY_NORMAL);
    }

    @Override
    public void onAccuracyChanged(Sensor sensor, int accuracy) {
        // don't care
    }

    private long now = 0;
    private long timeDiff = 0;
    private long lastUpdate = 0;
    private long lastShake = 0;

    private float x = 0;
    private float y = 0;
    private float z = 0;
    private float lastX = 0;
    private float lastY = 0;
    private float lastZ = 0;
    private float force = 0;
    
    private float threshold = 35.0f;
    
    private long intervalNanos = 1000 * 1000 * 10000; // 10sec
    
    private long timeToNextShakeMs = 10 * 1000;
    private long lastShakeTimestamp = 0L;
    
    @Override
    public void onSensorChanged(SensorEvent event) {
        if (event.sensor.getType() != Sensor.TYPE_ACCELEROMETER) {
            return;
        }
        
        now = event.timestamp;
        
        x = event.values[0];
        y = event.values[1];
        z = event.values[2];

        if (lastUpdate == 0) {
            // set some default vals
            lastUpdate = now;
            lastShake = now;
            lastX = x;
            lastY = y;
            lastZ = z;
        }
        else {
            timeDiff = now - lastUpdate;
            
            if (timeDiff > 0) { 
                force = Math.abs(x + y + z - lastX - lastY - lastZ);
                if (Float.compare(force, threshold) >0 ) {
                    if (now - lastShake >= intervalNanos && (System.currentTimeMillis() - lastShakeTimestamp) > timeToNextShakeMs) { 
                        Log.d(LOG_TAG, "Shaking detected.");
                        lastShakeTimestamp = System.currentTimeMillis();

                        SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(mContext);

                        if (preferences.getBoolean(mContext.getString(R.string.settings_key_use_rage_shake), true)) {
                            promptForReport();
                        }
                    }
                    else {
                        Log.d(LOG_TAG, "Suppress shaking - not passed interval. Ms to go: "+(timeToNextShakeMs - 
                                (System.currentTimeMillis() - lastShakeTimestamp))+" ms");
                    }
                    lastShake = now;
                }
                lastX = x;
                lastY = y;
                lastZ = z;
                lastUpdate = now; 
            }
        }
    }

}