/*
 * This file is part of LSPosed.
 *
 * LSPosed is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * LSPosed is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with LSPosed.  If not, see <https://www.gnu.org/licenses/>.
 *
 * Copyright (C) 2021 LSPosed Contributors
 */

package org.lsposed.lspd.service;

import android.app.ActivityThread;
import android.app.Notification;
import android.content.Context;
import android.ddm.DdmHandleAppName;
import android.os.Binder;
import android.os.Build;
import android.os.IBinder;
import android.os.IServiceManager;
import android.os.Looper;
import android.os.Parcel;
import android.os.Process;
import android.os.RemoteException;
import android.util.Log;

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

import com.android.internal.os.BinderInternal;

import org.lsposed.daemon.BuildConfig;
import org.lsposed.lspd.util.FakeContext;

import java.io.File;
import java.lang.AbstractMethodError;
import java.lang.Class;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import hidden.HiddenApiBridge;

public class ServiceManager {
    public static final String TAG = "LSPosedService";
    private static final File globalNamespace = new File("/proc/1/root");
    @SuppressWarnings("FieldCanBeLocal")
    private static LSPosedService mainService = null;
    private static LSPApplicationService applicationService = null;
    private static LSPManagerService managerService = null;
    private static LSPSystemServerService systemServerService = null;
    private static LogcatService logcatService = null;
    private static Dex2OatService dex2OatService = null;

    private static final ExecutorService executorService = Executors.newSingleThreadExecutor();

    @RequiresApi(Build.VERSION_CODES.Q)
    public static Dex2OatService getDex2OatService() {
        return dex2OatService;
    }

    public static ExecutorService getExecutorService() {
        return executorService;
    }

    private static void waitSystemService(String name) {
        while (android.os.ServiceManager.getService(name) == null) {
            try {
                Log.i(TAG, "service " + name + " is not started, wait 1s.");
                //noinspection BusyWait
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                Log.i(TAG, Log.getStackTraceString(e));
            }
        }
    }

    public static IServiceManager getSystemServiceManager() {
        return IServiceManager.Stub.asInterface(HiddenApiBridge.Binder_allowBlocking(BinderInternal.getContextObject()));
    }

    // call by ourselves
    public static void start(String[] args) {
        if (!ConfigFileManager.tryLock()) System.exit(0);

        int systemServerMaxRetry = 1;
        for (String arg : args) {
            if (arg.startsWith("--system-server-max-retry=")) {
                try {
                    systemServerMaxRetry = Integer.parseInt(arg.substring(arg.lastIndexOf('=') + 1));
                } catch (Throwable ignored) {
                }
            }
        }
        Log.i(TAG, "starting server...");
        Log.i(TAG, String.format("version %s (%d)", BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE));

        Thread.setDefaultUncaughtExceptionHandler((t, e) -> {
            Log.e(TAG, "Uncaught exception", e);
            System.exit(1);
        });

        logcatService = new LogcatService();
        logcatService.start();

        // get config before package service is started
        // otherwise getInstance will trigger module/scope cache
        var configManager = ConfigManager.getInstance();
        // --- DO NOT call ConfigManager.getInstance later!!! ---

        // Unblock log watchdog before starting anything else
        if (configManager.isLogWatchdogEnabled())
            logcatService.enableWatchdog();

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R)
            permissionManagerWorkaround();

        Process.setThreadPriority(Process.THREAD_PRIORITY_FOREGROUND);
        Looper.prepareMainLooper();


        mainService = new LSPosedService();
        applicationService = new LSPApplicationService();
        managerService = new LSPManagerService();
        systemServerService = new LSPSystemServerService(systemServerMaxRetry);
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
            dex2OatService = new Dex2OatService();
            dex2OatService.start();
        }

        systemServerService.putBinderForSystemServer();

        ActivityThread.systemMain();

        DdmHandleAppName.setAppName("org.lsposed.daemon", 0);

        waitSystemService("package");
        waitSystemService("activity");
        waitSystemService(Context.USER_SERVICE);
        waitSystemService(Context.APP_OPS_SERVICE);

        ConfigFileManager.reloadConfiguration();

        notificationWorkaround();

        BridgeService.send(mainService, new BridgeService.Listener() {
            @Override
            public void onSystemServerRestarted() {
                Log.w(TAG, "system restarted...");
            }

            @Override
            public void onResponseFromBridgeService(boolean response) {
                if (response) {
                    Log.i(TAG, "sent service to bridge");
                } else {
                    Log.w(TAG, "no response from bridge");
                }
                systemServerService.maybeRetryInject();
            }

            @Override
            public void onSystemServerDied() {
                Log.w(TAG, "system server died");
                systemServerService.putBinderForSystemServer();
                managerService.onSystemServerDied();
            }
        });

        // Force logging on boot, now let's see if we need to stop logging
        if (!configManager.verboseLog()) {
            logcatService.stopVerbose();
        }

        Looper.loop();
        throw new RuntimeException("Main thread loop unexpectedly exited");
    }

    public static LSPApplicationService getApplicationService() {
        return applicationService;
    }

    public static LSPApplicationService requestApplicationService(int uid, int pid, String processName, IBinder heartBeat) {
        if (applicationService.registerHeartBeat(uid, pid, processName, heartBeat))
            return applicationService;
        else return null;
    }

    public static LSPManagerService getManagerService() {
        return managerService;
    }

    public static LogcatService getLogcatService() {
        return logcatService;
    }

    public static boolean systemServerRequested() {
        return systemServerService.systemServerRequested();
    }

    public static File toGlobalNamespace(File file) {
        return new File(globalNamespace, file.getAbsolutePath());
    }

    public static File toGlobalNamespace(String path) {
        if (path == null) return null;
        if (path.startsWith("/")) return new File(globalNamespace, path);
        else return toGlobalNamespace(new File(path));
    }

    public static boolean existsInGlobalNamespace(File file) {
        return toGlobalNamespace(file).exists();
    }

    public static boolean existsInGlobalNamespace(String path) {
        return toGlobalNamespace(path).exists();
    }

    private static void permissionManagerWorkaround() {
        try {
            Field sCacheField = android.os.ServiceManager.class.getDeclaredField("sCache");
            sCacheField.setAccessible(true);
            var sCache = (Map) sCacheField.get(null);
            sCache.put("permissionmgr", new BinderProxy("permissionmgr"));
            sCache.put("legacy_permission", new BinderProxy("legacy_permission"));
            sCache.put("appops", new BinderProxy("appops"));
        } catch (Throwable e) {
            Log.e(TAG, "failed to init permission manager", e);
        }
    }

    private static void notificationWorkaround() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
            try {
                Class feature = Class.forName("android.app.FeatureFlagsImpl");
                Field systemui_is_cached = feature.getDeclaredField("systemui_is_cached");
                systemui_is_cached.setAccessible(true);
                systemui_is_cached.set(null, true);
                Log.d(TAG, "set flag systemui_is_cached to true");
            } catch (Throwable e) {
                Log.e(TAG, "failed to change feature flags", e);
            }
        }

        try {
            new Notification.Builder(new FakeContext(), "notification_workaround").build();
        } catch (AbstractMethodError e) {
            FakeContext.nullProvider = ! FakeContext.nullProvider;
        } catch (Throwable e) {
            Log.e(TAG, "failed to build notifications", e);
        }

    }

    private static class BinderProxy extends Binder {
        private static final Method rawGetService;

        static {
            try {
                rawGetService = android.os.ServiceManager.class.getDeclaredMethod("rawGetService", String.class);
                rawGetService.setAccessible(true);
            } catch (NoSuchMethodException e) {
                throw new RuntimeException(e);
            }
        }
        private IBinder mReal = null;
        private final String mName;

        BinderProxy(String name) {
            mName = name;
        }

        @Override
        protected boolean onTransact(int code, @NonNull Parcel data, @Nullable Parcel reply, int flags) throws RemoteException {
            synchronized (this) {
                if (mReal == null) {
                    try {
                        mReal = (IBinder) rawGetService.invoke(null, mName);
                    } catch (IllegalAccessException | InvocationTargetException ignored){

                    }
                }
                if (mReal != null) {
                    return mReal.transact(code, data, reply, flags);
                }
            }
            // getSplitPermissions
            if (reply != null && mName.equals("permissionmgr"))
                reply.writeTypedList(List.of());
            return true;
        }
    }
}
