/*
See LICENSE folder for this sample’s licensing information.

Abstract:
A command-line utility that runs Linux in a virtual machine.
*/

import Foundation
import Virtualization

class Delegate: NSObject {
}


class MySocketListenerDelegate: NSObject, VZVirtioSocketListenerDelegate {
    func listener(_ listener: VZVirtioSocketListener, shouldAcceptNewConnection connection: VZVirtioSocketConnection, from device: VZVirtioSocketDevice) -> Bool {
        // Implement your logic to decide whether to accept the new connection
        // For example, you may always accept:
        return true

        // Or, you might want to check some properties of the connection or device and decide based on that
    }
}


extension Delegate: VZVirtualMachineDelegate {
    func guestDidStop(_ virtualMachine: VZVirtualMachine) {
        print("The guest shut down. Exiting.")
        exit(EXIT_SUCCESS)
    }
}
if #available(macOS 13.0, *) {
    
    let vmBundlePath = NSHomeDirectory() + "/.guix/guix.bundle/"
    let mainDiskImagePath = vmBundlePath + "guix-raw.img"
    let efiVariableStorePath = vmBundlePath + "NVRAM"
    let machineIdentifierPath = vmBundlePath + "MachineIdentifier"
    var needsInstall = true
    
    // MARK: Parse the Command Line


    // MARK: Create the Virtual Machine Configuration
    print(shell("/bin/zsh ~/.guix/startup-script.sh"))

    
    if !FileManager.default.fileExists(atPath: vmBundlePath) {
        needsInstall = true
        //    createVMBundle()
        print(shell("mkdir ~/.guix && mkdir ~/.guix/home"))
        
        print(shell("cp -r /Applications/Guix.app/Contents/Resources/home/ssh-cert ~/.guix/"))
        
        print(shell("cp /Applications/Guix.app/Contents/Resources/home/*.sh ~/.guix/"))
        do {
            try FileManager.default.createDirectory(atPath: vmBundlePath, withIntermediateDirectories: false)
        } catch {
            fatalError("Failed to create “GUI Linux VM.bundle.”")
        }
        print("The main system image is now downloading. This may take a few minutes...")
        //    createMainDiskImage()
        let url = URL(string: "https://objectstorage.us-phoenix-1.oraclecloud.com/n/axfgkze2xif1/b/guix-system/o/msg-system-aarch64guix-raw.img.tar.gz")
        FileDownloader.loadFileSync(url: url!) { (path, error) in
            print("File downloaded to : \(path!)")
        }
        print(shell("tar -xvzf ~/.guix/guix.bundle/guix.tar.gz -C ~/.guix/guix.bundle/"))
        //print(shell("/opt/homebrew/bin/qemu-img convert ~/.guix/guix.bundle/guix-raw.qcow2 ~/.guix/guix.bundle/guix-raw.img"))
        
        
        //    configureAndStartVirtualMachine()
    } else {
        needsInstall = false
        //    configureAndStartVirtualMachine()
    }
    
    let configuration = VZVirtualMachineConfiguration()
    let platform = VZGenericPlatformConfiguration()
    let bootloader = VZEFIBootLoader()
    let disksArray = NSMutableArray()
    
    //set cpu count
    let totalAvailableCPUs = ProcessInfo.processInfo.processorCount
    
    var virtualCPUCount = totalAvailableCPUs <= 1 ? 1 : totalAvailableCPUs - 1
    virtualCPUCount = max(virtualCPUCount, VZVirtualMachineConfiguration.minimumAllowedCPUCount)
    virtualCPUCount = min(virtualCPUCount, VZVirtualMachineConfiguration.maximumAllowedCPUCount)
    configuration.cpuCount = virtualCPUCount
    
    //set memory size
    var memorySize = (4 * 1024 * 1024 * 1024) as UInt64 // 4 GiB
    memorySize = max(memorySize, VZVirtualMachineConfiguration.minimumAllowedMemorySize)
    memorySize = min(memorySize, VZVirtualMachineConfiguration.maximumAllowedMemorySize)
    configuration.memorySize = memorySize // 2 GiB
    
    
    if needsInstall {
        // This is a fresh install: Create a new machine identifier and EFI variable store,
        // and configure a USB mass storage device to boot the ISO image.
        
        let machineIdentifier = VZGenericMachineIdentifier()
        
        
        // Store the machine identifier to disk so you can retrieve it for subsequent boots.
        try! machineIdentifier.dataRepresentation.write(to: URL(fileURLWithPath: machineIdentifierPath))
        
        platform.machineIdentifier = machineIdentifier
        
        guard let efiVariableStore = try? VZEFIVariableStore(creatingVariableStoreAt: URL(fileURLWithPath: efiVariableStorePath)) else {
            fatalError("Failed to create the EFI variable store.")
        }
        
        bootloader.variableStore = efiVariableStore
        //            disksArray.add(createUSBMassStorageDeviceConfiguration())
    } else {
        // The VM is booting from a disk image that already has the OS installed.
        // Retrieve the machine identifier and EFI variable store that were saved to
        // disk during installation.
        // Retrieve the machine identifier.
        guard let machineIdentifierData = try? Data(contentsOf: URL(fileURLWithPath: machineIdentifierPath)) else {
            fatalError("Failed to retrieve the machine identifier data.")
        }

        guard let machineIdentifier = VZGenericMachineIdentifier(dataRepresentation: machineIdentifierData) else {
            fatalError("Failed to create the machine identifier.")
        }
        
        platform.machineIdentifier = machineIdentifier
        
        if !FileManager.default.fileExists(atPath: efiVariableStorePath) {
            fatalError("EFI variable store does not exist.")
        }
        
        bootloader.variableStore = VZEFIVariableStore(url: URL(fileURLWithPath: efiVariableStorePath))
    }
    
    configuration.platform = platform
    configuration.bootLoader = bootloader
    


    
    // Disk management
    guard let mainDiskAttachment = try? VZDiskImageStorageDeviceAttachment(url: URL(fileURLWithPath: mainDiskImagePath), readOnly: false) else {
        fatalError("Failed to create main disk attachment.")
    }

    let mainDisk = VZVirtioBlockDeviceConfiguration(attachment: mainDiskAttachment)
    
    disksArray.add(mainDisk)
    guard let disks = disksArray as? [VZStorageDeviceConfiguration] else {
        fatalError("Invalid disksArray.")
    }
    configuration.storageDevices = disks


    // Setup network
    let networkDevice = VZVirtioNetworkDeviceConfiguration()
    networkDevice.attachment = VZNATNetworkDeviceAttachment()
    configuration.networkDevices = [networkDevice]

    // TODO: setup audioDevices
    
    // Shared dirs
    let projectsURL = URL(fileURLWithPath: "/Users")

    let sharedDirectory = VZSharedDirectory(url: projectsURL, readOnly: false)
    let singleDirectoryShare = VZSingleDirectoryShare(directory: sharedDirectory)

    // Create the VZVirtioFileSystemDeviceConfiguration and assign it a unique tag.
    let sharingConfiguration = VZVirtioFileSystemDeviceConfiguration(tag: "Users")
    sharingConfiguration.share = singleDirectoryShare
    configuration.directorySharingDevices = [sharingConfiguration]
    
    //configuration.serialPorts = [ createConsoleConfiguration() ]
    //configuration.bootLoader = createBootLoader(kernelURL: kernelURL, initialRamdiskURL: initialRamdiskURL)
    
    let inputAudioDevice = VZVirtioSoundDeviceConfiguration()

    let inputStream = VZVirtioSoundDeviceInputStreamConfiguration()
    inputStream.source = VZHostAudioInputStreamSource()

    inputAudioDevice.streams = [inputStream]
    
    let outputAudioDevice = VZVirtioSoundDeviceConfiguration()

    let outputStream = VZVirtioSoundDeviceOutputStreamConfiguration()
    outputStream.sink = VZHostAudioOutputStreamSink()

    outputAudioDevice.streams = [outputStream]
    
    configuration.audioDevices = [inputAudioDevice, outputAudioDevice]
    
//    configuration.socketDevices = [VZVirtioSocketDeviceConfiguration()]
//
//
    do {
        try configuration.validate()
    } catch {
        print("Failed to validate the virtual machine configuration. \(error)")
        exit(EXIT_FAILURE)
    }
    
    // MARK: Instantiate and Start the Virtual Machine
    
    let virtualMachine = VZVirtualMachine(configuration: configuration)
//    let socketDevice = virtualMachine.socketDevices[0] as! VZVirtioSocketDevice

    let delegate = Delegate()
//    let socketListener = VZVirtioSocketListener()
//    let socketDelegate = MySocketListenerDelegate()
//    socketListener.delegate = socketDelegate

    background(delay: 3.0, background: {
        print(shell("/bin/zsh ~/.guix/running-script.sh"))

    }, completion: {

        print("Guix is now running and Graphical apps can be forwarded (if xquartz was installed)")
        // when background job finishes, wait 3 seconds and do something in main thread
    })

    
    DispatchQueue.main.async {
        virtualMachine.delegate = delegate
//        socketDevice.setSocketListener(socketListener, forPort: 80)

        virtualMachine.start { (result) in
            if case let .failure(error) = result {
                print("Failed to start the virtual machine. \(error)")
                exit(EXIT_FAILURE)
            }
//            if case .success() = result {
//
//                sleep(30)
//
//                socketDevice.connect(toPort: 80) { result in
//                    dump(result)
//                }
//            }
        }

    }



    RunLoop.main.run(until: Date.distantFuture)
    
    // MARK: - Virtual Machine Delegate

    
    func shell(_ command: String) -> String {
        let task = Process()
        let pipe = Pipe()
        
        task.standardOutput = pipe
        task.standardError = pipe
        task.arguments = ["-c", command]
        task.launchPath = "/bin/zsh"
        task.standardInput = nil
        task.launch()
        
        let data = pipe.fileHandleForReading.readDataToEndOfFile()
        let output = String(data: data, encoding: .utf8)!
        
        return output
    }
    func background(delay: Double = 0.0, background: (()->Void)? = nil, completion: (() -> Void)? = nil) {
            DispatchQueue.global(qos: .background).async {
                background?()
                if let completion = completion {
                    DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: {
                        completion()
                    })
                }
            }
        }

    
} else {
    print("only available on macos 13 or later")
}