# Copyright 2021 Erfan Abdi
# SPDX-License-Identifier: GPL-3.0-or-later
import logging
import os
from tools import helpers
import tools.config

import sys
import threading
import multiprocessing
import select
import queue
import time
import dbus
import dbus.service
from gi.repository import GLib

def is_initialized(args):
    return os.path.isfile(args.config) and os.path.isdir(tools.config.defaults["rootfs"])

def get_vendor_type(args):
    vndk_str = helpers.props.host_get(args, "ro.vndk.version")
    ret = "MAINLINE"
    if vndk_str != "":
        vndk = int(vndk_str)
        if vndk > 19:
            ret = "HALIUM_" + str(vndk - 19)

    return ret

def setup_config(args):
    cfg = tools.config.load(args)
    args.arch = helpers.arch.host()
    cfg["waydroid"]["arch"] = args.arch

    preinstalled_images_paths = tools.config.defaults["preinstalled_images_paths"]
    if not args.images_path:
        for preinstalled_images in preinstalled_images_paths:
            if os.path.isdir(preinstalled_images):
                if os.path.isfile(preinstalled_images + "/system.img") and os.path.isfile(preinstalled_images + "/vendor.img"):
                    args.images_path = preinstalled_images
                    break
                else:
                    logging.warning("Found directory {} but missing system or vendor image, ignoring...".format(preinstalled_images))

    if not args.images_path:
        args.images_path = tools.config.defaults["images_path"]
    cfg["waydroid"]["images_path"] = args.images_path

    channels_cfg = tools.config.load_channels()
    if not args.system_channel:
        args.system_channel = channels_cfg["channels"]["system_channel"]
    if not args.vendor_channel:
        args.vendor_channel = channels_cfg["channels"]["vendor_channel"]
    if not args.rom_type:
        args.rom_type = channels_cfg["channels"]["rom_type"]
    if not args.system_type:
        args.system_type = channels_cfg["channels"]["system_type"]

    args.system_ota = args.system_channel + "/" + args.rom_type + \
        "/waydroid_" + args.arch + "/" + args.system_type + ".json"
    system_request = helpers.http.retrieve(args.system_ota)
    if system_request[0] != 200:
        if args.images_path not in preinstalled_images_paths:
            raise ValueError(
                "Failed to get system OTA channel: {}, error: {}".format(args.system_ota, system_request[0]))
        else:
            args.system_ota = "None"

    device_codename = helpers.props.host_get(args, "ro.product.device")
    args.vendor_type = None
    for vendor in [device_codename, get_vendor_type(args)]:
        vendor_ota = args.vendor_channel + "/waydroid_" + \
            args.arch + "/" + vendor.replace(" ", "_") + ".json"
        vendor_request = helpers.http.retrieve(vendor_ota)
        if vendor_request[0] == 200:
            args.vendor_type = vendor
            args.vendor_ota = vendor_ota
            break

    if not args.vendor_type:
        if args.images_path not in preinstalled_images_paths:
            raise ValueError(
                "Failed to get vendor OTA channel: {}".format(vendor_ota))
        else:
            args.vendor_ota = "None"
            args.vendor_type = get_vendor_type(args)

    if args.system_ota != cfg["waydroid"].get("system_ota"):
        cfg["waydroid"]["system_datetime"] = tools.config.defaults["system_datetime"]
    if args.vendor_ota != cfg["waydroid"].get("vendor_ota"):
        cfg["waydroid"]["vendor_datetime"] = tools.config.defaults["vendor_datetime"]

    cfg["waydroid"]["vendor_type"] = args.vendor_type
    cfg["waydroid"]["system_ota"] = args.system_ota
    cfg["waydroid"]["vendor_ota"] = args.vendor_ota
    helpers.drivers.setupBinderNodes(args)
    cfg["waydroid"]["binder"] = args.BINDER_DRIVER
    cfg["waydroid"]["vndbinder"] = args.VNDBINDER_DRIVER
    cfg["waydroid"]["hwbinder"] = args.HWBINDER_DRIVER
    tools.config.save(args, cfg)

def init(args):
    if not is_initialized(args) or args.force:
        initializer_service = None
        try:
            initializer_service = tools.helpers.ipc.DBusContainerService("/Initializer", "id.waydro.Initializer")
        except dbus.DBusException:
            pass
        setup_config(args)
        status = "STOPPED"
        if os.path.exists(tools.config.defaults["lxc"] + "/waydroid"):
            status = helpers.lxc.status(args)
        if status != "STOPPED":
            logging.info("Stopping container")
            try:
                container = tools.helpers.ipc.DBusContainerService()
                args.session = container.GetSession()
                container.Stop(False)
            except Exception as e:
                logging.debug(e)
                tools.actions.container_manager.stop(args)
        if args.images_path not in tools.config.defaults["preinstalled_images_paths"]:
            helpers.images.get(args)
        else:
            helpers.images.remove_overlay(args)
        if not os.path.isdir(tools.config.defaults["rootfs"]):
            os.mkdir(tools.config.defaults["rootfs"])
        if not os.path.isdir(tools.config.defaults["overlay"]):
            os.mkdir(tools.config.defaults["overlay"])
            os.mkdir(tools.config.defaults["overlay"]+"/vendor")
        if not os.path.isdir(tools.config.defaults["overlay_rw"]):
            os.mkdir(tools.config.defaults["overlay_rw"])
            os.mkdir(tools.config.defaults["overlay_rw"]+"/system")
            os.mkdir(tools.config.defaults["overlay_rw"]+"/vendor")
        helpers.drivers.probeAshmemDriver(args)
        helpers.lxc.setup_host_perms(args)
        helpers.lxc.set_lxc_config(args)
        helpers.lxc.make_base_props(args)
        if status != "STOPPED":
            logging.info("Starting container")
            try:
                container.Start(args.session)
            except Exception as e:
                logging.debug(e)
                logging.error("Failed to restart container. Please do so manually.")

        if "running_init_in_service" not in args or not args.running_init_in_service:
            try:
                if initializer_service:
                    initializer_service.Done()
            except dbus.DBusException:
                pass
    else:
        logging.info("Already initialized")

def wait_for_init(args):
    helpers.ipc.create_channel("remote_init_output")

    mainloop = GLib.MainLoop()
    dbus_obj = DbusInitializer(mainloop, dbus.SystemBus(), '/Initializer', args)
    mainloop.run()

    # After init
    dbus_obj.remove_from_connection()

class DbusInitializer(dbus.service.Object):
    def __init__(self, looper, bus, object_path, args):
        self.args = args
        self.looper = looper
        dbus.service.Object.__init__(self, bus, object_path)

    @dbus.service.method("id.waydro.Initializer", in_signature='a{ss}', out_signature='', sender_keyword="sender", connection_keyword="conn")
    def Init(self, params, sender=None, conn=None):
        channels_cfg = tools.config.load_channels()
        no_auth = params["system_channel"] == channels_cfg["channels"]["system_channel"] and \
                  params["vendor_channel"] == channels_cfg["channels"]["vendor_channel"]
        if no_auth or ensure_polkit_auth(sender, conn, "id.waydro.Initializer.Init"):
            threading.Thread(target=remote_init_server, args=(self.args, params)).start()
        else:
            raise PermissionError("Polkit: Authentication failed")

    @dbus.service.method("id.waydro.Initializer", in_signature='', out_signature='')
    def Done(self):
        if is_initialized(self.args):
            self.looper.quit()

def ensure_polkit_auth(sender, conn, privilege):
    dbus_info = dbus.Interface(conn.get_object("org.freedesktop.DBus", "/org/freedesktop/DBus/Bus", False), "org.freedesktop.DBus")
    pid = dbus_info.GetConnectionUnixProcessID(sender)
    polkit = dbus.Interface(dbus.SystemBus().get_object("org.freedesktop.PolicyKit1", "/org/freedesktop/PolicyKit1/Authority", False), "org.freedesktop.PolicyKit1.Authority")
    try:
        (is_auth, _, _) = polkit.CheckAuthorization(
            ("unix-process", {
                "pid": dbus.UInt32(pid, variant_level=1),
                "start-time": dbus.UInt64(0, variant_level=1)}),
            privilege, {"AllowUserInteraction": "true"},
            dbus.UInt32(1),
            "",
            timeout=300)
        return is_auth
    except dbus.DBusException:
        raise PermissionError("Polkit: Authentication timed out")

def background_remote_init_process(args):
    with helpers.ipc.open_channel("remote_init_output", "wb") as channel_out:
        class StdoutRedirect(logging.StreamHandler):
            def write(self, s):
                channel_out.write(str.encode(s))
            def flush(self):
                pass
            def emit(self, record):
                if record.levelno >= logging.INFO:
                    self.write(self.format(record) + self.terminator)

        out = StdoutRedirect()
        sys.stdout = sys.stderr = out
        logging.getLogger().addHandler(out)

        ctl_queue = queue.Queue()
        def try_init(args):
            try:
                init(args)
            except Exception as e:
                print(str(e))
            finally:
                ctl_queue.put(0)

        def poll_pipe():
            poller = select.poll()
            poller.register(channel_out, select.POLLERR)
            poller.poll()
            # When reaching here the client was terminated
            ctl_queue.put(0)

        init_thread = threading.Thread(target=try_init, args=(args,))
        init_thread.daemon = True
        init_thread.start()

        poll_thread = threading.Thread(target=poll_pipe)
        poll_thread.daemon = True
        poll_thread.start()

        # Join any one of the two threads
        # Then exit the subprocess to kill the remaining thread.
        # Can you believe this is the only way to kill a thread in python???
        ctl_queue.get()

        sys.stdout = sys.__stdout__
        sys.stderr = sys.__stderr__
        logging.getLogger().removeHandler(out)

def remote_init_server(args, params):
    args.force = True
    args.images_path = ""
    args.rom_type = ""
    args.system_channel = params["system_channel"]
    args.vendor_channel = params["vendor_channel"]
    args.system_type = params["system_type"]
    args.running_init_in_service = True

    p = multiprocessing.Process(target=background_remote_init_process, args=(args,))
    p.daemon = True
    p.start()
    p.join()

def remote_init_client(args):
    # Local imports cause Gtk is intrusive
    import gi
    gi.require_version("Gtk", "3.0")
    from gi.repository import Gtk

    bus = dbus.SystemBus()

    if is_initialized(args):
        try:
            tools.helpers.ipc.DBusContainerService("/Initializer", "id.waydro.Initializer").Done()
        except dbus.DBusException:
            pass
        return

    def notify_and_quit(caller):
        if is_initialized(args):
            try:
                tools.helpers.ipc.DBusContainerService("/Initializer", "id.waydro.Initializer").Done()
            except dbus.DBusException:
                pass
        GLib.idle_add(Gtk.main_quit)

    class WaydroidInitWindow(Gtk.Window):
        def __init__(self):
            super().__init__(title="Initialize Waydroid")
            channels_cfg = tools.config.load_channels()

            self.set_default_size(600, 250)
            self.set_icon_name("waydroid")

            grid = Gtk.Grid(row_spacing=6, column_spacing=6, margin=10, column_homogeneous=True)
            grid.set_hexpand(True)
            grid.set_vexpand(True)
            self.add(grid)

            sysOtaLabel = Gtk.Label("System OTA")
            sysOtaEntry = Gtk.Entry()
            sysOtaEntry.set_text(channels_cfg["channels"]["system_channel"])
            grid.attach(sysOtaLabel, 0, 0, 1, 1)
            grid.attach_next_to(sysOtaEntry ,sysOtaLabel, Gtk.PositionType.RIGHT, 2, 1)
            self.sysOta = sysOtaEntry.get_buffer()

            vndOtaLabel = Gtk.Label("Vendor OTA")
            vndOtaEntry = Gtk.Entry()
            vndOtaEntry.set_text(channels_cfg["channels"]["vendor_channel"])
            grid.attach(vndOtaLabel, 0, 1, 1, 1)
            grid.attach_next_to(vndOtaEntry, vndOtaLabel, Gtk.PositionType.RIGHT, 2, 1)
            self.vndOta = vndOtaEntry.get_buffer()

            sysTypeLabel = Gtk.Label("Android Type")
            sysTypeCombo = Gtk.ComboBoxText()
            sysTypeCombo.set_entry_text_column(0)
            for t in ["VANILLA", "GAPPS"]:
                sysTypeCombo.append_text(t)
            sysTypeCombo.set_active(0)
            grid.attach(sysTypeLabel, 0, 2, 1, 1)
            grid.attach_next_to(sysTypeCombo, sysTypeLabel, Gtk.PositionType.RIGHT, 2, 1)
            self.sysType = sysTypeCombo

            downloadBtn = Gtk.Button("Download")
            downloadBtn.connect("clicked", self.on_download_btn_clicked)
            grid.attach(downloadBtn, 1,3,1,1)
            self.downloadBtn = downloadBtn

            doneBtn = Gtk.Button("Done")
            doneBtn.connect("clicked", lambda x: self.destroy())
            doneBtn.get_style_context().add_class('suggested-action')
            grid.attach_next_to(doneBtn, downloadBtn, Gtk.PositionType.RIGHT, 1, 1)
            self.doneBtn = doneBtn

            outScrolledWindow = Gtk.ScrolledWindow()
            outScrolledWindow.set_hexpand(True)
            outScrolledWindow.set_vexpand(True)
            outTextView = Gtk.TextView()
            outTextView.set_property('editable', False)
            outTextView.set_property('cursor-visible', False)
            outScrolledWindow.add(outTextView)
            grid.attach(outScrolledWindow, 0, 4, 3, 1)
            self.outScrolledWindow = outScrolledWindow
            self.outTextView = outTextView
            self.outBuffer = outTextView.get_buffer()
            self.outBuffer.create_mark("end", self.outBuffer.get_end_iter(), False)

            self.open_channel = None

        def scroll_to_bottom(self):
            self.outTextView.scroll_mark_onscreen(self.outBuffer.get_mark("end"))

        def on_download_btn_clicked(self, widget):
            widget.set_sensitive(False)
            self.doneBtn.hide()
            self.outTextView.show()
            init_params = (self.sysOta.get_text(), self.vndOta.get_text(), self.sysType.get_active_text())
            init_runner = threading.Thread(target=self.run_init, args=init_params)
            init_runner.daemon = True
            init_runner.start()

        def run_init(self, systemOta, vendorOta, systemType):
            def draw_sync(s):
                if s.startswith('\r'):
                    last = self.outBuffer.get_iter_at_line(self.outBuffer.get_line_count()-1)
                    last.backward_char()
                    self.outBuffer.delete(last, self.outBuffer.get_end_iter())
                self.outBuffer.insert(self.outBuffer.get_end_iter(), s)
                self.scroll_to_bottom()
            def draw(s):
                GLib.idle_add(draw_sync, s)

            if self.open_channel is not None:
                self.open_channel.close()
                # Wait for other end to reset
                time.sleep(1)

            draw("Waiting for waydroid container service...\n")
            try:
                params = {
                    "system_channel": self.sysOta.get_text(),
                    "vendor_channel": self.vndOta.get_text(),
                    "system_type": self.sysType.get_active_text()
                }
                tools.helpers.ipc.DBusContainerService("/Initializer", "id.waydro.Initializer").Init(params, timeout=310)
            except dbus.DBusException as e:
                if e.get_dbus_name() == "org.freedesktop.DBus.Python.PermissionError":
                    draw(e.get_dbus_message().splitlines()[-1] + "\n")
                else:
                    draw("The waydroid container service is not listening\n")
                GLib.idle_add(self.downloadBtn.set_sensitive, True)
                return

            with helpers.ipc.open_channel("remote_init_output", "rb") as channel:
                self.open_channel = channel
                GLib.idle_add(self.downloadBtn.set_sensitive, True)
                line = ""
                try:
                    while True:
                        data = channel.read(1)
                        if len(data) == 0:
                            draw(line)
                            break
                        c = data.decode()
                        if c == '\r':
                            draw(line)
                            line = c
                        else:
                            line += c
                            if c == '\n':
                                draw(line)
                                line = ""
                except:
                    draw("\nInterrupted\n")

            if is_initialized(args):
                GLib.idle_add(self.doneBtn.show)
                draw("Done\n")


    GLib.set_prgname("Waydroid")
    win = WaydroidInitWindow()
    win.connect("destroy", notify_and_quit)

    win.show_all()
    win.outTextView.hide()
    win.doneBtn.hide()

    Gtk.main()
