import lldb
import os
import re
from lldb import SBCommandReturnObject, SBError, SBExpressionOptions, SBFileSpec, SBFrame, eLanguageTypeObjC
from reveallldb.custom_exceptions import *
from reveallldb.supported_target import SupportedTarget
from typing import List, Optional

class RevealLoader(object):
    # This path assumes that the script is stored in Reveal's Application Support directory under 'Scripts'.
    _applicationSupportDirectory = os.path.expanduser("~/Library/Application Support/Reveal/")
    _localFrameworkPath = os.path.join(_applicationSupportDirectory, 'RevealServer', 'RevealServer.xcframework')

    def __init__(self, frame: SBFrame):
        self.frame = frame
        self.expressionOptions = SBExpressionOptions()
        self.expressionOptions.SetLanguage(eLanguageTypeObjC)
        self.expressionOptions.SetSuppressPersistentResult(True)

        process = frame.thread.process
        debugger = process.target.debugger

        # Check if the process is running an incompatible target
        assert self._target_is_compatible(), "target {0} is not supported by Reveal Server.".format(process.target.triple)

        # Check that process is not stopped
        assert debugger.StateIsStoppedState(process.state), "process must be paused to execute Reveal Server commands."

        if not self._process_main_thread_contains_frame_named("UIApplicationMain"):
            if not self._process_main_thread_contains_frame_named("NSExtensionMain"):
                raise Error("process is not yet ready to execute Reveal Server commands.")
            else:
                raise Error("process is running an unsupported application extension. Cannot load Reveal Server.")

    def local_framework_binary_path(self) -> Optional[str]:
        variants = self._directory_names_in_directory(self._localFrameworkPath)
        variant = self._framework_slice_name(variants)
        return os.path.join(self._localFrameworkPath, variant, "RevealServer.framework/RevealServer")

    def remote_framework_binary_path(self) -> Optional[str]:
        privateFrameworksPath = self.get_remote_private_frameworks_path()

        if privateFrameworksPath is not None and privateFrameworksPath != "<nil>":
            frameworkPath: str = os.path.join(privateFrameworksPath, "RevealServer.framework/RevealServer")
            return self.remote_get_realpath(frameworkPath)

        else:
            return None

    def remote_file_exists(self, remoteFilePath: str) -> bool:
        expression = '(BOOL)[[objc_getClass("NSFileManager") defaultManager] fileExistsAtPath:@"{0}"] != NO'.format(remoteFilePath)
        return self.frame.EvaluateExpression(expression, self.expressionOptions).value == "true"

    def remote_get_realpath(self, remoteFilePath: str) -> str:
        expression = '[@"{0}" stringByResolvingSymlinksInPath]'.format(remoteFilePath)
        return self.frame.EvaluateExpression(expression, self.expressionOptions).GetObjectDescription()

    def get_remote_private_frameworks_path(self) -> Optional[str]:
        expression = '(NSString *)[[objc_getClass("NSBundle") mainBundle] privateFrameworksPath]'
        return self.frame.EvaluateExpression(expression, self.expressionOptions).GetObjectDescription()

    def is_server_loaded(self) -> bool:
        expression = "(void*)dlsym((void*)-2, \"OBJC_CLASS_$_IBARevealLoader\")"
        value = self.frame.EvaluateExpression(expression, self.expressionOptions).value

        if value is None:
            return False
        else:
            # Attempt to convert the returned hexadecimal base-16 value to an int
            try:
                pointerValue = int(str(value), 16)
                return pointerValue != 0
            except:
                return False

    def server_protocol_version(self) -> int:
        expression = "(int)[objc_getClass(\"IBARevealProtocol\") protocolVersion]"
        return self.frame.EvaluateExpression(expression, self.expressionOptions).signed

    def inject_server(self, result: SBCommandReturnObject):
        # If the target is running in the Simulator, load Reveal Server from the local host
        if self._target_is_simulator():
            binaryPath = self.local_framework_binary_path()
            if binaryPath is None:
                return

        else:
            # If target is running on device, check if Reveal Server framework is included in the bundle and try loading it if it exists
            binaryPath = self.remote_framework_binary_path()
            if binaryPath is None or not self.remote_file_exists(binaryPath):
                result.SetError("failed to load Reveal Server because it was not found in the remote application bundle. For information about debugging apps with Reveal on device, please refer to the Integration Guide.")
                return

        # Check that Reveal Server is already loaded first
        if self.is_server_loaded():
            result.AppendWarning("not loading Reveal Server, it's already loaded.")
            return

        # Load Reveal Server image from the specified path
        result.AppendMessage("Loading Reveal Server from {0}…".format(binaryPath))
        error = SBError()
        self.frame.thread.process.LoadImage(SBFileSpec(binaryPath), error)

        if error.fail:
            result.SetError(error)

    def is_server_running(self) -> bool:
        expression = "(BOOL)[[[objc_getClass(\"IBARevealServer\") sharedServer] httpServer] isRunning] != NO"
        return self.frame.EvaluateExpression(expression, self.expressionOptions).value == "true"

    def start_server(self, result: SBCommandReturnObject):
        if self.is_server_loaded():
            self.frame.EvaluateExpression("(void)[objc_getClass(\"IBARevealLoader\") startServer]", self.expressionOptions)
        else:
            result.SetError("Reveal Server is not loaded. Use `reveal load` command first.")

    def stop_server(self, result: SBCommandReturnObject):
        if self.is_server_loaded():
            self.frame.EvaluateExpression("(void)[objc_getClass(\"IBARevealLoader\") stopServer]", self.expressionOptions)
        else:
            result.AppendWarning("Reveal Server is not loaded, there's nothing to stop.")

    def setup_legacy_breakpoints_if_necessary(self, autostart: bool, result: SBCommandReturnObject):
        if not self._is_legacy_breakpoint_at_process_first_frame():
            return

        if autostart:
            callback_name = load_reveal_autostart_callback.__qualname__
        else:
            callback_name = load_reveal_callback.__qualname__

        target = self.frame.thread.process.target

        # Import the callback so that LLDB can see it:
        target.debugger.HandleCommand(
            "script from %s import %s" % (__name__, callback_name)
        )

        symbol_names = [
            '-\[UIViewController viewDidLoad\]'
            # Add more locations here if required
        ]

        breakpoint_registration_count = 0

        for symbol_name in symbol_names:
            breakpoint = target.BreakpointCreateByRegex(symbol_name, "UIKitCore")
            breakpoint.SetScriptCallbackFunction(callback_name)
            breakpoint.SetAutoContinue(True)
            breakpoint.SetOneShot(True)
            if breakpoint.GetNumResolvedLocations() == 0:
                target.BreakpointDelete(breakpoint.GetID())
            else:
                result.AppendMessage("Reveal Server created one-shot breakpoint '{0}'".format(symbol_name))
                breakpoint_registration_count += breakpoint.GetNumResolvedLocations()

        if breakpoint_registration_count == 0:
            result.SetError("Could not find any breakpoints to register for the Reveal Server framework. Tried the following symbols: {0}".format(", ".join(symbol_names)))

    def _directory_names_in_directory(self, path: str) -> List[str]:
        return [ f for f in os.listdir(path) if os.path.isdir(os.path.join(path, f)) ]

    def _framework_slice_name(self, variants: List[str]) -> str:
        target_triple = self.frame.thread.process.target.triple
        target = SupportedTarget.from_lldb_target_triple(target_triple)

        if target is None:
            raise Exception("The current process type '{0}' is unsupported.".format(target_triple))

        framework_slice = target.framework_slice(variants)

        if framework_slice is None:
            raise Exception("Could not locate a Reveal Server framework variant for the current target process of type '{0}'. \n\nDid find the following directories within RevealServer.xcframework: \n - {1}.".format(target_triple, '\n - '.join(variants)))

        return framework_slice

    def _is_legacy_breakpoint_at_process_first_frame(self) -> bool:
        return self._process_first_frame_contains("UIApplicationMain") or self._process_first_frame_contains("-[UIApplication _run]")

    def _process_main_thread_contains_frame_named(self, frameName: str) -> bool:
        mainThreadFrames = self.frame.thread.process.GetThreadAtIndex(0).frames
        for frame in mainThreadFrames:
            if frame.name == frameName:
                return True

        return False

    def _process_first_frame_contains(self, frameName: str) -> bool:
        mainThreadFrames = self.frame.thread.process.GetThreadAtIndex(0).frames
        return mainThreadFrames[0].name == frameName

    def _target_is_compatible(self) -> bool:
        architecture = self.frame.thread.process.target.triple
        return "apple-ios" in architecture or "apple-tvos" in architecture or "apple-watchos" in architecture or "apple-xros" in architecture

    def _target_is_simulator(self) -> bool:
        platformName = self.frame.thread.process.target.platform.GetName()
        return platformName.endswith("simulator")

def load_reveal_callback(frame, bp_loc, internal_dict) -> str:
    return "reveal load"

def load_reveal_autostart_callback(frame, bp_loc, internal_dict) -> str:
    return "reveal load --autostart"
