#!/usr/bin/env python3

# This script's main purpose is to handle the following:
#   - Download PatcherSupportPkg resources
#   - Convert payloads directory into DMG (GUI only)
#   - Build Binary via Pyinstaller
#   - Add Launcher.sh (TUI only)
#   - Patch 'LC_VERSION_MIN_MACOSX' to OS X 10.10
#   - Add commit data to Info.plist

# Copyright (C) 2022 - Mykola Grymalyuk

from pathlib import Path
import time
import argparse
import os
import subprocess
import plistlib
import time
import sys

from resources import constants

class create_binary:

    def __init__(self):
        start = time.time()
        print("- Starting build script")
        self.set_cwd()
        self.args = self.parse_arguments()

        self.preflight_processes()
        self.build_binary()
        self.postflight_processes()
        print(f"- Build script completed in {str(round(time.time() - start, 2))} seconds")

    def set_cwd(self):
        os.chdir(Path(__file__).resolve().parent)
        print(f"- Current Working Directory: \n\t{os.getcwd()}")

    def parse_arguments(self):
        parser = argparse.ArgumentParser(description='Builds OpenCore-Patcher binary')
        parser.add_argument('--build_tui', action='store_true', help='Builds TUI binary, if omitted GUI binary is built')
        parser.add_argument('--branch', type=str, help='Git branch name')
        parser.add_argument('--commit', type=str, help='Git commit URL')
        parser.add_argument('--commit_date', type=str, help='Git commit date')
        parser.add_argument('--reset_binaries', action='store_true', help='Force redownload and imaging of payloads')
        args = parser.parse_args()
        return args

    def setup_pathing(self):
        python_path = sys.executable
        python_binary = python_path.split("/")[-1]
        python_bin_dir = python_path.strip(python_binary)

        # macOS (using Python installed by homebrew (e.g. brew))
        if f"/usr/local/opt/python@3." in sys.executable:
            print(f"\t* NOTE: home(brew) python3 detected; using (sys.exec_prefix, python_path) ==> {sys.exec_prefix, python_path}")
            # - under brew, pip3 will install pyinstaller at:
            #   /usr/local/lib/python3.9/site-packages/pyinstaller
            #   and /usr/local/bin/pyinstaller stub to load and run.

            pyinstaller_path = f"/usr/local/bin/pyinstaller"
        else:
            pyinstaller_path = f"{python_bin_dir}pyinstaller"

        if not Path(pyinstaller_path).exists():
            print(f"  - pyinstaller not found:\n\t{pyinstaller_path}")
            raise Exception("pyinstaller not found")

        self.pyinstaller_path = pyinstaller_path

    def preflight_processes(self):
        print("- Starting preflight processes")
        self.setup_pathing()
        self.delete_extra_binaries()
        self.download_resources()
        if not self.args.build_tui:
            # payloads.dmg is only needed for GUI builds
            self.generate_payloads_dmg()

    def postflight_processes(self):
        print("- Starting postflight processes")
        if self.args.build_tui:
            self.move_launcher()
        self.patch_load_command()
        self.add_commit_data()

    def build_binary(self):
        if Path(f"./dist/OpenCore-Patcher.app").exists():
            print("- Found OpenCore-Patcher.app, removing...")
            rm_output = subprocess.run(
                ["rm", "-rf", "./dist/OpenCore-Patcher.app"],
                stdout=subprocess.PIPE, stderr=subprocess.PIPE
            )
            if rm_output.returncode != 0:
                print("- Remove failed")
                print(rm_output.stderr.decode('utf-8'))
                raise Exception("Remove failed")


        if self.args.build_tui:
            print("- Building TUI binary...")
            build_args = [self.pyinstaller_path, "./OpenCore-Patcher.spec", "--noconfirm"]
        else:
            print("- Building GUI binary...")
            build_args = [self.pyinstaller_path, "./OpenCore-Patcher-GUI.spec", "--noconfirm"]

        build_result = subprocess.run(build_args, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
        if build_result.returncode != 0:
            print("- Build failed")
            print(build_result.stderr.decode('utf-8'))
            raise Exception("Build failed")

    def delete_extra_binaries(self):
        delete_files = [
            "AutoPkg-Assets.pkg",
            "AutoPkg-Assets.pkg.zip",
            "InstallAssistant.pkg",
        ]
        print("- Deleting extra binaries...")
        for file in Path("payloads").glob(pattern="*"):
            if file.name in delete_files:
                print(f"  - Deleting {file.name}")
                file.unlink()
            elif (Path(file) / Path("Contents/Resources/createinstallmedia")).exists():
                print(f"  - Deleting {file}")
                subprocess.run(["rm", "-rf", file])

    def download_resources(self):
        patcher_support_pkg_version = constants.Constants().patcher_support_pkg_version
        required_resources = [
            "Universal-Binaries.zip"
        ]

        print("- Downloading required resources...")
        for resource in required_resources:
            if Path(f"./payloads/{resource}").exists():
                if self.args.reset_binaries:
                    print(f"  - Removing old {resource}")
                    rm_output = subprocess.run(
                        ["rm", "-rf", f"./payloads/{resource}"],
                        stdout=subprocess.PIPE, stderr=subprocess.PIPE
                    )
                    if rm_output.returncode != 0:
                        print("- Remove failed")
                        print(rm_output.stderr.decode('utf-8'))
                        raise Exception("Remove failed")
                else:
                    print(f"  - {resource} already exists, skipping download")
                    continue
            print(f"  - Downloading {resource}...")

            download_result = subprocess.run(
                [
                    "curl", "-LO",
                    f"https://github.com/dortania/PatcherSupportPkg/releases/download/{patcher_support_pkg_version}/{resource}"
                ],
                stdout=subprocess.PIPE, stderr=subprocess.PIPE
            )

            if download_result.returncode != 0:
                print("  - Download failed")
                print(download_result.stderr.decode('utf-8'))
                raise Exception("Download failed")
            if not Path(f"./{resource}").exists():
                print(f"  - {resource} not found")
                raise Exception(f"{resource} not found")

            print("  - Moving into payloads")
            mv_output = subprocess.run(["mv", resource, "./payloads/"], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
            if mv_output.returncode != 0:
                print("  - Move failed")
                print(mv_output.stderr.decode('utf-8'))
                raise Exception("Move failed")

    def generate_payloads_dmg(self):
        if Path("./payloads.dmg").exists():
            if self.args.reset_binaries:
                print("  - Removing old payloads.dmg")
                rm_output = subprocess.run(
                    ["rm", "-rf", "./payloads.dmg"],
                    stdout=subprocess.PIPE, stderr=subprocess.PIPE
                )
                if rm_output.returncode != 0:
                    print("- Remove failed")
                    print(rm_output.stderr.decode('utf-8'))
                    raise Exception("Remove failed")
            else:
                print("  - payloads.dmg already exists, skipping creation")
                return
        print("  - Generating DMG...")
        dmg_output = subprocess.run([
            'hdiutil', 'create', './payloads.dmg',
            '-megabytes', '32000',
            '-format', 'UDZO', '-ov',
            '-volname', 'payloads',
            '-fs', 'HFS+',
            '-srcfolder', './payloads',
            '-passphrase', 'password', '-encryption'
        ], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
        if dmg_output.returncode != 0:
            print("  - DMG generation failed")
            print(dmg_output.stderr.decode('utf-8'))
            raise Exception("DMG generation failed")

        print("  - DMG generation complete")

    def add_commit_data(self):
        if not self.args.branch and not self.args.commit and not self.args.commit_date:
            print("  - No commit data provided, adding source info")
            branch = "Built from source"
            commit_url = ""
            commit_date = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
        else:
            branch = self.args.branch
            commit_url = self.args.commit
            commit_date = self.args.commit_date
        print("  - Adding commit data to Info.plist")
        plist_path = Path("./dist/OpenCore-Patcher.app/Contents/Info.plist")
        plist = plistlib.load(Path(plist_path).open("rb"))
        plist["Github"] = {
            "Branch": branch,
            "Commit URL": commit_url,
            "Commit Date": commit_date,
        }
        plistlib.dump(plist, Path(plist_path).open("wb"), sort_keys=True)

    def patch_load_command(self):
        # Patches LC_VERSION_MIN_MACOSX in Load Command to report 10.10
        #
        # By default Pyinstaller will create binaries supporting 10.13+
        # However this limitation is entirely arbitrary for our libraries
        # and instead we're able to support 10.10 without issues.
        #
        # To verify set version:
        #   otool -l ./dist/OpenCore-Patcher.app/Contents/MacOS/OpenCore-Patcher
        #
        #       cmd LC_VERSION_MIN_MACOSX
        #   cmdsize 16
        #   version 10.13
        #       sdk 10.9
        print("  - Patching LC_VERSION_MIN_MACOSX")
        path = './dist/OpenCore-Patcher.app/Contents/MacOS/OpenCore-Patcher'
        find = b'\x00\x0D\x0A\x00' # 10.13 (0xA0D)
        replace = b'\x00\x0A\x0A\x00' # 10.10 (0xA0A)
        with open(path, 'rb') as f:
            data = f.read()
            data = data.replace(find, replace, 1)
            with open(path, 'wb') as f:
                f.write(data)

    def move_launcher(self):
        print("  - Adding TUI launcher")
        mv_output = subprocess.run(
            ["cp", "./payloads/launcher.sh", "./dist/OpenCore-Patcher.app/Contents/MacOS/Launcher"],
            stdout=subprocess.PIPE, stderr=subprocess.PIPE
        )
        if mv_output.returncode != 0:
            print("  - Move failed")
            print(mv_output.stderr.decode('utf-8'))
            raise Exception("Move failed")

if __name__ == "__main__":
    create_binary()