/*
 * Copyright (C) 2016 Peter Gregus for GravityBox Project (C3C076@xda)
 * 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.wrbug.gravitybox.nougat.telecom;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import com.wrbug.gravitybox.nougat.BuildConfig;
import com.wrbug.gravitybox.nougat.GravityBoxSettings;

import android.content.Context;
import android.hardware.Sensor;
import android.hardware.SensorManager;
import android.os.Handler;
import android.os.PowerManager;
import android.os.Vibrator;
import android.os.PowerManager.WakeLock;
import android.telecom.TelecomManager;
import de.robv.android.xposed.XC_MethodHook;
import de.robv.android.xposed.XSharedPreferences;
import de.robv.android.xposed.XposedBridge;
import de.robv.android.xposed.XposedHelpers;

public class CallFeatures {
    private static final String TAG = "GB:CallFeatures";
    private static final boolean DEBUG = BuildConfig.DEBUG;

    private static final String CLASS_CALLS_MANAGER = "com.android.server.telecom.CallsManager";
    private static final String CLASS_CALL = "com.android.server.telecom.Call";

    private static void log(String message) {
        XposedBridge.log(TAG + ": " + message);
    }

    public static CallFeatures init(XSharedPreferences prefs, ClassLoader classLoader) throws Throwable {
        return new CallFeatures(prefs, classLoader);
    }

    private XSharedPreferences mPrefs;
    private int mFlipAction = GravityBoxSettings.PHONE_FLIP_ACTION_NONE;
    private Set<String> mCallVibrations;
    private Context mContext;
    private SensorManager mSensorManager;
    private boolean mSensorListenerAttached = false;
    private Object mIncomingCall;
    private Object mOutgoingCall;
    private List<Object> mActiveCallList = new ArrayList<Object>();
    private Vibrator mVibrator;
    private Handler mHandler;
    private WakeLock mWakeLock;

    private CallFeatures() { /* must be created by calling init() */ }

    private CallFeatures(XSharedPreferences prefs, ClassLoader classLoader) throws Throwable {
        mPrefs = prefs;
        refreshPrefs();
        createHooks(classLoader);
    }

    private void createHooks(ClassLoader classLoader) throws Throwable {
        Class<?> clsCallsManager = XposedHelpers.findClass(CLASS_CALLS_MANAGER, classLoader);

        XposedBridge.hookAllConstructors(clsCallsManager, new XC_MethodHook() {
            @Override
            protected void afterHookedMethod(MethodHookParam param) throws Throwable {
                onCallsManagerCreated(param.thisObject);
            }
        });

        XposedHelpers.findAndHookMethod(clsCallsManager, "addCall", CLASS_CALL, new XC_MethodHook() {
            @Override
            protected void afterHookedMethod(MethodHookParam param) throws Throwable {
                onCallAdded(param.args[0]);
            }
        });

        XposedHelpers.findAndHookMethod(clsCallsManager, "setCallState",
                CLASS_CALL, int.class, String.class, new XC_MethodHook() {
            @Override
            protected void afterHookedMethod(MethodHookParam param) throws Throwable {
                onCallStateChanged(param.args[0], (int)param.args[1]);
            }
        });
    }

    private void onCallsManagerCreated(Object callsManager) {
        if (DEBUG) log("onCallsManagerCreated()");
        mContext = (Context) XposedHelpers.getObjectField(callsManager, "mContext");
        mSensorManager = (SensorManager) mContext.getSystemService(Context.SENSOR_SERVICE);
        mVibrator = (Vibrator) mContext.getSystemService(Context.VIBRATOR_SERVICE);
        mHandler = new Handler();
        PowerManager pm = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
        mWakeLock  = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG);
    }

    private void onCallAdded(Object call) {
        refreshPrefs();
        int state = (int) XposedHelpers.callMethod(call, "getState");
        if (DEBUG) log("onCallAdded: state = " + CallState.toString(state));
        onCallStateChanged(call, state);
    }

    public void onCallStateChanged(Object call, int state) {
        if (DEBUG) log("onStateChanged: " +
                "is our incoming call: " + (call == mIncomingCall) +
                "; is our outgoing call: " + (call == mOutgoingCall) +
                "; state=" + CallState.toString(state));
        // keep track of active calls
        if (state == CallState.ACTIVE && !mActiveCallList.contains(call)) {
            mActiveCallList.add(call);
        }
        // flip actions for incoming call
        if (state == CallState.RINGING) {
            if (mIncomingCall == null) {
                mIncomingCall = call;
                attachSensorListener();
            }
        } else if (call == mIncomingCall) {
            mIncomingCall = null;
            detachSensorListener();
        }
        // vibrate for waiting call
        if (state == CallState.RINGING && mActiveCallList.size() > 0 &&
                mCallVibrations.contains(GravityBoxSettings.CV_WAITING)) {
            if (DEBUG) log("Vibrating for waiting incoming call");
            vibrate(200, 300, 500);
        }
        // register outgoing call
        if (state == CallState.DIALING && mOutgoingCall == null) {
            mOutgoingCall = call;
        }
        // vibrate on outgoing connected and periodic
        if (state == CallState.ACTIVE && call == mOutgoingCall) {
            if (mCallVibrations.contains(GravityBoxSettings.CV_CONNECTED)) {
                if (DEBUG) log("Outgoing call connected; executing vibrate on call connected");
                vibrate(100, 0, 0);
            }
            if (mCallVibrations.contains(GravityBoxSettings.CV_PERIODIC) &&
                    mHandler != null) {
                if (DEBUG) log("Outgoing call connected; starting periodic vibrations");
                mHandler.postDelayed(mPeriodicVibrator, 45000);
                if (mWakeLock != null) {
                    mWakeLock.acquire(46000);
                    if (DEBUG) log("Partial Wake Lock acquired");
                }
            }
        }
        // handle call disconnected
        if (state == CallState.DISCONNECTED) {
            if (mActiveCallList.contains(call)) {
                mActiveCallList.remove(call);
            }
            if (mCallVibrations.contains(GravityBoxSettings.CV_DISCONNECTED)) {
                if (DEBUG) log("Call disconnected; executing vibrate on call disconnected");
                vibrate(50, 100, 50);
            }
            if (call == mOutgoingCall) {
                if (DEBUG) log("Our outgoing call disconnected");
                mOutgoingCall = null;
                if (mHandler != null) {
                    mHandler.removeCallbacks(mPeriodicVibrator);
                }
                if (mWakeLock != null && mWakeLock.isHeld()) {
                    mWakeLock.release();
                    if (DEBUG) log("Partial Wake Lock released");
                }
            }
        }
    }

    private PhoneSensorEventListener mPhoneSensorEventListener = 
            new PhoneSensorEventListener(new PhoneSensorEventListener.ActionHandler() {
        @Override
        public void onFaceUp() {
            if (DEBUG) log("PhoneSensorEventListener.onFaceUp");
            // do nothing
        }

        @Override
        public void onFaceDown() {
            if (DEBUG) log("PhoneSensorEventListener.onFaceDown");

            try {
                switch (mFlipAction) {
                    case GravityBoxSettings.PHONE_FLIP_ACTION_MUTE:
                        if (DEBUG) log("Muting call");
                        silenceRinger();
                        break;
                    case GravityBoxSettings.PHONE_FLIP_ACTION_DISMISS:
                        if (DEBUG) log("Rejecting call");
                        rejectCall(mIncomingCall);
                        break;
                    case GravityBoxSettings.PHONE_FLIP_ACTION_NONE:
                    default:
                        // do nothing
                }
            } catch (Throwable t) {
                XposedBridge.log(t);
            }
        }
    });

    private void attachSensorListener() {
        if (mSensorManager == null || 
                mSensorListenerAttached ||
                mFlipAction == GravityBoxSettings.PHONE_FLIP_ACTION_NONE) return;

        mPhoneSensorEventListener.reset();
        mSensorManager.registerListener(mPhoneSensorEventListener, 
                mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER), 
                SensorManager.SENSOR_DELAY_NORMAL);
        mSensorListenerAttached = true;

        if (DEBUG) log("Sensor listener attached");
    }

    private void detachSensorListener() {
        if (mSensorManager == null || !mSensorListenerAttached) return;

        mSensorManager.unregisterListener(mPhoneSensorEventListener);
        mSensorListenerAttached = false;

        if (DEBUG) log("Sensor listener detached");
    }

    private void silenceRinger() {
        try {
            TelecomManager tm = (TelecomManager) mContext.getSystemService(Context.TELECOM_SERVICE);
            tm.silenceRinger();
        } catch(Throwable t) {
            XposedBridge.log(t);
        }
    }

    private void rejectCall(Object call) {
        if (call == null) return;

        try {
            XposedHelpers.callMethod(call, "reject", false, null);
            if (DEBUG) log("Call rejected");
        } catch (Throwable t) {
            XposedBridge.log(t);
        }
    }

    private void vibrate(int v1, int p1, int v2) {
        if (mVibrator == null) return;

        long[] pattern = new long[] { 0, v1, p1, v2 };
        mVibrator.vibrate(pattern, -1);
    }

    private Runnable mPeriodicVibrator = new Runnable() {
        @Override
        public void run() {
            if (mWakeLock != null) {
                if (mWakeLock.isHeld()) {
                    mWakeLock.release();
                }
                mWakeLock.acquire(61000);
                if (DEBUG) log("Partial Wake Lock timeout extended");
            }
            vibrate(50, 0, 0);
            mHandler.postDelayed(this, 60000);
        }
    };

    private void refreshPrefs() {
        mPrefs.reload();
        mCallVibrations = mPrefs.getStringSet(
                GravityBoxSettings.PREF_KEY_CALL_VIBRATIONS, new HashSet<String>());
        if (DEBUG) log("mCallVibrations = " + mCallVibrations.toString());

        mFlipAction = GravityBoxSettings.PHONE_FLIP_ACTION_NONE;
        try {
            mFlipAction = Integer.valueOf(mPrefs.getString(
                    GravityBoxSettings.PREF_KEY_PHONE_FLIP, "0"));
            if (DEBUG) log("mFlipAction = " + mFlipAction);
        } catch (NumberFormatException e) {
            XposedBridge.log(e);
        }
    }
}