package com.ryuunoakaihitomi.rebootmenu.util.hook;

import android.Manifest;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.pm.PackageManager;
import android.os.Binder;
import android.os.Build;
import android.os.Handler;
import android.os.Parcel;
import android.os.PowerManager;
import android.os.Process;
import android.os.RemoteException;
import android.os.SystemClock;
import android.os.SystemProperties;
import android.os.SystemService;
import android.text.TextUtils;
import android.util.Log;

import com.android.internal.util.DumpUtils;
import com.ryuunoakaihitomi.rebootmenu.BuildConfig;
import com.ryuunoakaihitomi.rebootmenu.IRMPowerActionService;
import com.ryuunoakaihitomi.rebootmenu.util.StringUtils;

import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import static android.os.Build.VERSION.SDK_INT;
import static android.os.Build.VERSION_CODES.M;
import static android.os.Build.VERSION_CODES.N;
import static android.os.Build.VERSION_CODES.O;

/**
 * IRMPowerActionService AIDL的具体实现
 * Created by ZQY on 2019/1/3.
 */

@TargetApi(M)
final class RMPowerActionService extends IRMPowerActionService.Stub {

    static final String TAG = "RMPowerActionService";

    private final Context mContext;
    private PackageManager mPackageManager;
    private PowerManager mPowerManager;
    //API比aidl稳定,但是导包是个问题,现在暂时使用反射将就下
    private Method goToSleep, rebootSafeMode, shutdown;

    private final RMPowerActionRecord record;

    RMPowerActionService(Context context) {
        Log.d(TAG, "Constructor: " + context);
        mContext = context;
        record = RMPowerActionRecord.instance;
    }

    @Override
    public void lockScreen() {
        preliminaryPreparation("lockScreen");
        injectSystemThread(() -> invokeNoThrowAndReturn(goToSleep, mPowerManager, SystemClock.uptimeMillis()));
    }

    @Override
    public void reboot(String reason) {
        preliminaryPreparation("reboot", reason);
        injectSystemThread(() -> mPowerManager.reboot(reason));
    }

    @TargetApi(M)
    @Override
    public void safeMode() {
        preliminaryPreparation("safeMode");
        injectSystemThread(() -> {
            if (SDK_INT < N) {
                //ShutdownThread.java
                // Indicates whether we are rebooting into safe mode
                //public static final String REBOOT_SAFEMODE_PROPERTY = "persist.sys.safemode";
                SystemProperties.set("persist.sys.safemode", "1");
                mPowerManager.reboot(null);
                return;
            }
            invokeNoThrowAndReturn(rebootSafeMode, mPowerManager);
        });
    }

    @TargetApi(M)
    @Override
    public void shutdown() {
        preliminaryPreparation("shutdown");
        injectSystemThread(() -> {
            Object[] params = SDK_INT >= N ?
                    new Object[]{false, null, false} : new Object[]{false, false};
            invokeNoThrowAndReturn(shutdown, mPowerManager, params);
        });
    }

    @Override
    public void hotReboot() {
        preliminaryPreparation("hotReboot");
        injectSystemThread(() -> {
            //throw new IllegalStateException("Hot Reboot Request");    /*在此抛出异常也会导致热重启*/
            SystemService.restart("zygote");
        });
    }

    //测试服务状态
    @Override
    public void ping() {
        preliminaryPreparation("ping");
        Log.i(TAG, "ping: " + StringUtils.varArgsToString(mPowerManager, mPackageManager, mContext, Process.myUid(), getCallingPid(), getCallingUid()));
        injectSystemThread(() ->
                Log.i(TAG, "ping: " + StringUtils.varArgsToString(pingBinder(), getCallingPid(), getCallingUid())));
    }

    //For "dumpsys"
    @Override
    protected void dump(@NonNull FileDescriptor fd, @NonNull PrintWriter fOut, @Nullable String[] args) {
        if (!enforceDumpPermission(fOut)) return;
        if (args != null) {
            if (args.length == 0) {
                dumpInfo(fOut);
                return;
            }
            for (String arg : args) {
                switch (arg) {
                    //Help
                    case "-h":
                        fOut.println("RMPowerActionService dump options:");
                        fOut.println("  -c");
                        fOut.println("    Clear operation record.");
                        fOut.println(" [-d]");
                        fOut.println("    Print dump information.");
                        fOut.println("  -h");
                        fOut.println("    Print this help text.");
                        fOut.println("  -r");
                        fOut.println("    Print operation record.");
                        break;
                    //Clear record
                    case "-c":
                        record.clear();
                        fOut.println("Record cleared.");
                        break;
                    //show Record
                    case "-r":
                        fOut.println("PowerActionRecord:");
                        for (Map.Entry<Long, String> entry : record.traversal().entrySet()) {
                            fOut.println(entry.getKey() + "\t" + entry.getValue());
                        }
                        break;
                    //Dump info
                    case "-d":
                        dumpInfo(fOut);
                        break;
                    default:
                        fOut.println("Unknown arguments: " + arg);
                }
            }
        }
    }

    @Override
    public boolean onTransact(int code, Parcel data, Parcel reply, int flags) throws RemoteException {
        Log.v(TAG, "onTransact: code=" + code);
        return super.onTransact(code, data, reply, flags);
    }

    //插入到系统线程才有系统权限
    private void injectSystemThread(Runnable r) {
        new Handler(mContext.getMainLooper()).post(r);
    }

    //所有的系统服务都已经初始化完成
    @SuppressWarnings({"JavaReflectionMemberAccess", "unchecked"})
    @TargetApi(N)
    void allServicesInitialised() {
        Log.d(TAG, "allServicesInitialised");
        mPowerManager = mContext.getSystemService(PowerManager.class);
        //PackageManager无法用以下方法从system_server的context中获取
        //mPackageManager = mContext.getSystemService(PackageManager.class);
        mPackageManager = mContext.getPackageManager();
        Class pmCls = PowerManager.class;

        /*
         * Forces the device to go to sleep.
         * <p>
         * Overrides all the wake locks that are held.
         * This is what happens when the power key is pressed to turn off the screen.
         * </p><p>
         * Requires the {@link android.Manifest.permission#DEVICE_POWER} permission.
         * </p>
         *
         * @param time The time when the request to go to sleep was issued, in the
         * {@link SystemClock#uptimeMillis()} time base.  This timestamp is used to correctly
         * order the go to sleep request with other power management functions.  It should be set
         * to the timestamp of the input event that caused the request to go to sleep.
         *
         * @see #userActivity
         * @see #wakeUp
         *
         * @removed Requires signature permission.
         */
        goToSleep = getMethodNoThrow(pmCls, "goToSleep", long.class);

        /*
         * Reboot the device. Will not return if the reboot is successful.
         * <p>
         * Requires the {@link android.Manifest.permission#REBOOT} permission.
         * </p>
         * @hide
         */
        if (SDK_INT >= N)
            rebootSafeMode = getMethodNoThrow(pmCls, "rebootSafeMode");

        shutdown = SDK_INT >= N ?

                /*
                 * Turn off the device.
                 *
                 * @param confirm If true, shows a shutdown confirmation dialog.
                 * @param reason code to pass to android_reboot() (e.g. "userrequested"), or null.
                 * @param wait If true, this call waits for the shutdown to complete and does not return.
                 *
                 * @hide
                 */
                getMethodNoThrow(pmCls, "shutdown", boolean.class, String.class, boolean.class) :

                /*
                 * Turn off the device.
                 *
                 * @param confirm If true, shows a shutdown confirmation dialog.
                 * @param wait If true, this call waits for the shutdown to complete and does not return.
                 *
                 * @hide
                 */
                getMethodNoThrow(pmCls, "shutdown", boolean.class, boolean.class);

        Log.d(TAG, "allServicesInitialised: Methods:" + StringUtils.varArgsToString(goToSleep, rebootSafeMode, shutdown));
    }

    /**
     * 检测调用方
     */
    private boolean checkSelfInvoke() {
        int uid = Binder.getCallingUid();
        if (mPackageManager == null) {
            Log.e(TAG, "checkSelfInvoke: ", new IllegalStateException("mPackageManager not initialized"));
            return false;
        }
        if (uid == Process.SYSTEM_UID || uid == 0) {
            Log.i(TAG, "checkSelfInvoke: Invoked by uid(" + uid + ") with system privilege.");
            return true;
        }
        String[] packages = mPackageManager.getPackagesForUid(uid);
        if (packages != null && packages.length > 0) {
            for (String pn : packages) {
                Log.d(TAG, "checkSelfInvoke: pn=" + pn);
                if (!pn.startsWith(BuildConfig.APPLICATION_ID)) {
                    Log.w(TAG, "checkSelfInvoke: May be invoked by an illegal source.");
                    return false;
                }
            }
        } else {
            Log.e(TAG, "checkSelfInvoke: ", new NullPointerException("packages is null"));
            return false;
        }
        return true;
    }

    private boolean enforceDumpPermission(PrintWriter pw) {
        if (Build.VERSION.SDK_INT >= O) return DumpUtils.checkDumpPermission(mContext, TAG, pw);
        else {
            try {
                mContext.enforceCallingOrSelfPermission(Manifest.permission.DUMP, "enforceDumpPermission");
                return true;
            } catch (SecurityException e) {
                pw.println(StringUtils.printStackTraceToString(e));
                Log.e(TAG, "enforceDumpPermission: ", e);
            }
        }
        return false;
    }

    @SuppressWarnings("unchecked")
    private Method getMethodNoThrow(@NonNull Class sourceClass, String name, Class<?>... parameterTypes) {
        try {
            return sourceClass.getMethod(name, parameterTypes);
        } catch (NoSuchMethodException e) {
            Log.e(TAG, "getMethodNoThrow: ", e);
        }
        return null;
    }

    private void invokeNoThrowAndReturn(@NonNull Method method, Object instance, Object... args) {
        try {
            method.invoke(instance, args);
        } catch (Throwable throwable) {
            //IllegalAccessException|IllegalArgumentException|InvocationTargetException|NullPointerException
            Log.e(TAG, "invokeNoThrowAndReturn: ", throwable);
        }
    }

    private void dumpInfo(PrintWriter pw) {
        pw.print(this);
        pw.print(" dump info:");
        pw.println();
        int longestLen = 0;
        for (Field field : getClass().getDeclaredFields()) {
            try {
                String dmpLine = StringUtils.varArgsToString(field.getName(), field.get(this));
                if (longestLen < dmpLine.length()) longestLen = dmpLine.length();
                pw.println(dmpLine);
            } catch (IllegalAccessException e) {
                pw.println(e);
            }
        }
        //https://stackoverflow.com/questions/1235179/simple-way-to-repeat-a-string-in-java
        pw.println(TextUtils.join("", Collections.nCopies(longestLen, '-')));
    }

    /**
     * 前期准备:包括检查权限,打印信号日志,记录操作
     *
     * @param operationTag 操作标识
     * @param args         附加参数
     */
    private void preliminaryPreparation(String operationTag, Object... args) {
        if (!checkSelfInvoke()) throw new SecurityException("Permission Denied!");
        String reportBody = operationTag;
        if (args != null) reportBody += ' ' + StringUtils.varArgsToString(args);
        Log.i(TAG, reportBody);
        record.add(SystemClock.elapsedRealtime(), reportBody);
    }

    //操作记录
    private enum RMPowerActionRecord {

        instance;
        private @NonNull
        final
        ArrayList<Long> mOpTimes = new ArrayList<>();
        private @NonNull
        final
        ArrayList<String> mOpTags = new ArrayList<>();

        void add(Long time, String tag) {
            mOpTimes.add(time);
            mOpTags.add(tag);
        }

        void clear() {
            mOpTimes.clear();
            mOpTags.clear();
        }

        Map<Long, String> traversal() {
            Map<Long, String> ret = new LinkedHashMap<>();
            for (int i = 0; i < mOpTimes.size(); i++)
                ret.put(mOpTimes.get(i), mOpTags.get(i));
            return ret;
        }
    }
}