
from .blender_export import BuildContext, exportScene
from .utils_debug import debug_err, debug_log
import bpy
import os

from .external_process import exec_command
from .utils import deleteDirectoryRecursive, getWebProjectPath, openFile, projectIsSaved, getTemplatePath, copy_and_overwrite, openUrl, getChangeDirectoryCommand, getOpenDirectoryCommand, showMessage
from . import component_watcher
from .utils_web_project import getAssetsDirectory, runBuild, runInstall, serverIsRunning, startLocalServer, stopLocalServers, tryStopOtherServerProcesses
import subprocess
from .utils_npm import updatePackageVersions

from .server.server import startHttpServer, stopHttpServer


class buildDistOp(bpy.types.Operator):
    bl_idname = "needle.build_dist"
    bl_label = "Build Dist"
    bl_options = {"REGISTER"}
    bl_description = "Needle License Required\nBuilds the web project for distribution (so you can upload it to a server).\nA development build is uncompressed while a production build requires toktx to be installed and compresses the files which reduces size and load time significantly."
    
    production: bpy.props.BoolProperty(default=False)
    
    def execute(self, context):

        projectPath = getWebProjectPath()
        if not os.path.exists(projectPath):
            self.report(
                {'ERROR'}, "Web project does not exist at " + projectPath)
            return {"CANCELLED"}
        
        # delay this operation because blender's glTF exporter directly accesses bpy.context.scene
        # BUT bpy.context.scene does not change during the operator execution when we set window.scene
        # and bpy.context.scene is readonly - so to control which scene gets exported we need to delay the execution
        # see https://linear.app/needle/issue/NE-5175
        buildDistDelayed(self, projectPath, self.production)
        # runBuild(projectPath, self.production)
        return {"FINISHED"}

def buildDistDelayed(self, projectPath: str, production: bool, targetPlatform: str = "folder", postBuildCmd:str = None):
    # delay and return 0.1
    bpy.app.timers.register(lambda: buildDist(self, projectPath, production, targetPlatform, postBuildCmd), first_interval=0.1)

def buildDist(self, projectPath: str, production: bool, targetPlatform: str = "folder", postBuildCmd:str = None): 
    """
    Builds the web project for distribution (so you can upload it to a server).
    """
    # make sure to export the scene once again
    # rather export once again unnecessarily than having outdated export files when making a build
    # e.g. with old settings and extensions
    buildContext = BuildContext()
    buildContext.targetPlatform = targetPlatform
    exportScene(self, context=buildContext, building_dist=True)
    proc = runBuild(projectPath, production, postbuildcmd=postBuildCmd, openDirectory=targetPlatform == "folder")
    return None


class openWorkspaceOp(bpy.types.Operator):
    bl_idname = "scene.open_workspace"
    bl_label = "Open Workspace"
    bl_options = {"REGISTER"}
    bl_description = "Opens the vscode workspace for the web project"

    def execute(self, context):
        webProject = getWebProjectPath()
        if not os.path.exists(webProject):
            self.report(
                {'ERROR'}, "Web project does not exist at " + webProject + ", you might need to save the file first")
            return {"CANCELLED"}
        # iterate files ending  with .code-workspace
        for file in os.listdir(webProject):
            if file.endswith(".code-workspace"):
                webProject = webProject.replace("\\", "/")
                workspacePath = webProject + "/" + file
                print("OPEN WORKSPACE", workspacePath)
                
                openFile(workspacePath)
                component_watcher.startFileWatcher()
                showMessage("Opened workspace in VSCode", "Opened workspace", "INFO")
                return {"FINISHED"}
        
        try:
            # create a .code-workspace file
            workspacePath = os.path.join(webProject, "needle-workspace.code-workspace")
            with open(workspacePath, "w") as file:
                file.write('{\n\t"folders": [\n\t\t{\n\t\t\t"path": "."\n\t\t}\n\t]\n}')
            openFile(workspacePath)
            component_watcher.startFileWatcher()
            showMessage("Created workspace in VSCode", "Created workspace", "INFO")
            return {"FINISHED"}
        except Exception as e:
            debug_err("Error creating workspace", e)
            self.report({'ERROR'}, "Error creating workspace: " + str(e))

        
        showMessage("No workspace found in " + webProject, "No workspace found", "ERROR")
        openFile(webProject)
        return {"FINISHED"}


class startServerOp(bpy.types.Operator):
    bl_idname = "needle.start_server"
    bl_label = "Start Server"
    bl_options = {"REGISTER"}
    bl_description = "Creates and starts a local Needle Engine development web server for this scene"

    def execute(self, context):
        self.report({'INFO'}, "Starting server...")

        # if not projectIsSaved():
        #     self.report(
        #         {'ERROR'}, "Can not export: please save the file first")
        #     return {"CANCELLED"}

        # run server
        directory = getWebProjectPath()
        if not os.path.exists(directory):
            self.report(
                {'ERROR'}, "Web project does not exist at " + directory)
            return {"CANCELLED"}
        
        # stop the file watchers to unlock the compiler if it has to update
        component_watcher.stopFileWatchers()

        # run npm install command and show progress
        # subprocess.run("npm install", shell=True, cwd=directory)
        cmdPath = directory.replace("\\", "/")
        print("START SERVER IN DIRECTORY", cmdPath)

        startLocalServer(cmdPath)

        # export if assets has no glb file
        # if not os.path.exists(os.path.join(directory, "assets", "scene.glb")):
        bpy.ops.needle_scene.export()

        # restart the file watcher after a few seconds to make sure the server is running
        # and give it time to potentially update the packages
        def restartFileWatcher():
            if serverIsRunning():
                component_watcher.startFileWatcher()
        bpy.app.timers.register(restartFileWatcher, first_interval=10)
        return {"FINISHED"}
    

class startVanillaServerOp(bpy.types.Operator):
    bl_idname = "needle.start_vanilla_server"
    bl_label = "Start Server"
    bl_options = {"REGISTER"}
    bl_description = "Creates and starts a local development web server for this scene"

    def execute(self, context):
        webProjectDir = getWebProjectPath()
        if not os.path.exists(webProjectDir):
            self.report(
                {'ERROR'}, "Web project does not exist at " + webProjectDir)
            return {"CANCELLED"}
        self.report({'INFO'}, "Starting server...")
        exportScene(self, building_dist=False)
        try:
            startHttpServer(webProjectDir)
        except KeyboardInterrupt:
            # not sure why but when switching folders on windows (while a server is still running)
            # and clicking start server we get an keyboard interrupt here
            startHttpServer(webProjectDir)
        return {"FINISHED"}


class stopVanillaServerOp(bpy.types.Operator):
    bl_idname = "needle.stop_vanilla_server"
    bl_label = "Stop Server"
    bl_options = {"REGISTER"}

    def execute(self, context):
        stopHttpServer()
        return {"FINISHED"}

class stopLocalServerOp(bpy.types.Operator):
    bl_idname = 'needle.stop_server'
    bl_label = 'Stop local server'
    bl_options = {"REGISTER"}
    bl_description = "Stops the local Needle Engine server"

    def execute(self, context):
        self.report({'INFO'}, "Stopping server...")
        component_watcher.stopFileWatchers()
        stopLocalServers()
        return {"FINISHED"}


class openProjectOp(bpy.types.Operator):
    bl_idname = 'scene.needle_open_project'
    bl_label = 'Open Project'
    bl_options = {"REGISTER"}
    bl_description = "Open the Needle web project for this scene"

    def execute(self, context):
        webProject = getWebProjectPath()
        if webProject is None or not os.path.exists(webProject):
            self.report(
                {'ERROR'}, "Web project does not exist - you might need to save the file first")
            return {"CANCELLED"}
        self.report({'INFO'}, "Opening project " + webProject)
        openUrl(webProject)
        return {"FINISHED"}
    
def canGenerateProject(path: str):
    if not path or len(path) == 0: 
        return False
    # check if the path is a path (contains \\ or /)
    if path.find("\\") == -1 and path.find("/") == -1:
        return False
    # if the directory doesnt exist yet we can generate it
    if not os.path.exists(path):
        return True
    # if the directory exists it must be empty
    files = os.listdir(path)
    # ignore .DS_Store
    if ".DS_Store" in files:
        files.remove(".DS_Store")
    return len(files) == 0


isGeneratingProject = False
class generateProjectOp(bpy.types.Operator):
    bl_idname = 'needle.generate_project'
    bl_label = 'Generate Needle Engine Project'
    bl_options = {"REGISTER"}
    bl_description = "Generates a Needle Engine web project for this scene"

    templateName: bpy.props.StringProperty(default="vite")

    def execute(self, context):

        # because we call start server <> generate project from both functions
        # we safeguard that we dont run into a recursive loop
        global isGeneratingProject
        if isGeneratingProject:
            return {"FINISHED"}
        isGeneratingProject = True

        try:
            self.report({'INFO'}, "Generating project...")
            component_watcher.stopFileWatchers()
            targetDir = getWebProjectPath()
            # make sure the directory exists
            if not os.path.exists(targetDir):
                os.makedirs(targetDir)

            # make sure the directory has package.json
            packageJsonPath = os.path.join(targetDir, 'package.json')
            if not os.path.exists(packageJsonPath):
                print("No package.json found, copying from template")
                # template directory
                templateDir = getTemplatePath(self.templateName or "vite")
                # copy template recursively
                copy_and_overwrite(templateDir, targetDir)

            # open directory
            print("Project at \"" + targetDir + "\"")
            updatePackageVersions(targetDir)
            
            self.report({'INFO'}, "Generated project at " + targetDir)

            showMessage("Click Run Project to install and start the server", "Successfully generated web project. Hold on: Your web project is booting up", "INFO")

            # run start server
            bpy.ops.needle.start_server()

            return {"FINISHED"}
        
        except Exception as e:
            debug_err("Error generating project", e)
            self.report({'ERROR'}, "Error generating project: " + str(e))
        finally:
            isGeneratingProject = False


class installOp(bpy.types.Operator):
    bl_idname = 'needle.install'
    bl_label = 'Install Project packages'
    bl_options = {"REGISTER"}
    bl_description = "Runs npm install in the project directory. Hold ALT to make a clean install (deleting node_modules and package.lock.json before running installation)"

    altIsPressed = False

    def invoke(self, context, event):
        self.altIsPressed = event.alt
        return self.execute(context)
    
    def execute(self, context):
        self.report({'INFO'}, "Installing project...")
        webProject = getWebProjectPath(True)
        debug_log("INSTALL PROJECT", webProject)

        isCleanInstall = self.altIsPressed

        stopLocalServers()

        if webProject is None or not os.path.exists(webProject):
            self.report(
                {'ERROR'}, "Web project does not exist - you might need to save the file first")
            return {"CANCELLED"}

        if isCleanInstall:
            print("CLEAN INSTALL")
            # delete node_modules
            nodeModulesPath = os.path.join(webProject, "node_modules")
            deleteDirectoryRecursive(nodeModulesPath)

            # delete package-lock.json
            packageLockPath = os.path.join(webProject, "package-lock.json")
            if os.path.exists(packageLockPath):
                os.remove(packageLockPath)

        print("INSTALLING PROJECT: ", webProject)
        runInstall(webProject)

        return {"FINISHED"}
    
    
def hasAnyglTFOrGlbFiles():
    dir = getWebProjectPath()
    for file in os.listdir(dir):
        if file.endswith(".gltf") or file.endswith(".glb"):
            return True
    # search in assets folder
    assetDir = getAssetsDirectory(dir)
    if assetDir:
        assetsDir = os.path.join(dir, assetDir)
        if os.path.exists(assetsDir):
            for file in os.listdir(os.path.join(dir, assetDir)):
                if file.endswith(".gltf") or file.endswith(".glb"):
                    return True
    return False