#!/usr/bin/python3

import os
import sys
import shutil
import subprocess
from argparse import ArgumentParser
from utils import Utils
import imagegenerator

def runInstaller(options, install_config, working_directory):
    try:
        sys.path.insert(0, options.installer_path)
        from installer import Installer
    except:
        raise ImportError('Installer path incorrect!')

    # Run the installer
    installer = Installer(working_directory=working_directory, rpm_path=options.rpm_path,
                          log_path=options.log_path)
    installer.configure(install_config)
    installer.execute()

def get_file_name_with_last_folder(filename):
    basename = os.path.basename(filename)
    dirname = os.path.dirname(filename)
    lastfolder = os.path.basename(dirname)
    name = os.path.join(lastfolder, basename)
    return name

def create_pkg_list_to_copy_to_iso(build_install_option, output_data_path):
    option_list_json = Utils.jsonread(build_install_option)
    options_sorted = option_list_json.items()
    packages = []
    for install_option in options_sorted:
        if install_option[0] != "iso":
            file_path = os.path.join(output_data_path, os.path.splitext(install_option[1]["packagelist_file"])[0]+"_expanded.json")
            package_list_json = Utils.jsonread(file_path)
            packages = packages + package_list_json["packages"]
    return packages

def create_additional_file_list_to_copy_in_iso(base_path, build_install_option):
    option_list_json = Utils.jsonread(build_install_option)
    options_sorted = option_list_json.items()
    file_list = []
    for install_option in options_sorted:
        if "additional-files" in install_option[1]:
            file_list = file_list + list(map(
                lambda filename: os.path.join(base_path, filename),
                install_option[1].get("additional-files")))
    return file_list

    #copy_flags 1: add the rpm file for the package
    #           2: add debuginfo rpm file for the package.
    #           4: add src rpm file for the package
def create_rpm_list_to_be_copied_to_iso(pkg_to_rpm_map_file, build_install_option, copy_flags,
                                        output_data_path):
    packages = []
    if build_install_option is None:
        packages = []
    else:
        packages = create_pkg_list_to_copy_to_iso(build_install_option, output_data_path)

    rpm_list = []
    pkg_to_rpm_map = Utils.jsonread(pkg_to_rpm_map_file)
    for k in pkg_to_rpm_map:
        if build_install_option is None or k in packages:
            if not pkg_to_rpm_map[k]['rpm'] is None and bool(copy_flags & 1):
                filename = pkg_to_rpm_map[k]['rpm']
                rpm_list.append(get_file_name_with_last_folder(filename))
            if not pkg_to_rpm_map[k]['debugrpm'] is None and bool(copy_flags & 2):
                filename = pkg_to_rpm_map[k]['debugrpm']
                rpm_list.append(pkg_to_rpm_map[k]['debugrpm'])
            if not pkg_to_rpm_map[k]['sourcerpm'] is None and bool(copy_flags & 4):
                rpm_list.append(pkg_to_rpm_map[k]['sourcerpm'])
    return rpm_list

def make_debug_iso(working_directory, debug_iso_path, rpm_list):
    if os.path.exists(working_directory) and os.path.isdir(working_directory):
        shutil.rmtree(working_directory)
    process = subprocess.Popen(['mkdir', '-p', os.path.join(working_directory, "DEBUGRPMS")])
    process.wait()
    for rpmfile in rpm_list:
        if os.path.isfile(rpmfile):
            dirname = os.path.dirname(rpmfile)
            lastfolder = os.path.basename(dirname)
            dest_working_directory = os.path.join(working_directory, "DEBUGRPMS", lastfolder)
            if not os.path.isdir(dest_working_directory):
                process = subprocess.Popen(['mkdir', dest_working_directory])
                process.wait()
            shutil.copy2(rpmfile, dest_working_directory)
    process = subprocess.Popen(['mkisofs', '-r', '-o', debug_iso_path, working_directory])
    process.wait()
    shutil.rmtree(working_directory)

def make_src_iso(working_directory, src_iso_path, rpm_list):
    if os.path.exists(working_directory) and os.path.isdir(working_directory):
        shutil.rmtree(working_directory)
    process = subprocess.Popen(['mkdir', '-p', os.path.join(working_directory, "SRPMS")])
    process.wait()
    for rpmfile in rpm_list:
        if os.path.isfile(rpmfile):
            shutil.copy2(rpmfile, os.path.join(working_directory, "SRPMS"))
    process = subprocess.Popen(['mkisofs', '-r', '-o', src_iso_path, working_directory])
    process.wait()
    shutil.rmtree(working_directory)

def createIso(options):
    working_directory = os.path.abspath(os.path.join(options.stage_path, "photon_iso"))
    script_directory = os.path.dirname(os.path.realpath(__file__))
    # Making the iso if needed
    if options.iso_path:
        # Additional RPMs to copy to ISO's /RPMS/ folder
        rpm_list = " ".join(
            create_rpm_list_to_be_copied_to_iso(
                options.pkg_to_rpm_map_file,
                options.pkg_to_be_copied_conf_file, 1, options.generated_data_path))
        # Additional files to copy to ISO's / folder
        files_to_copy = " ".join(
            create_additional_file_list_to_copy_in_iso(
                os.path.abspath(options.stage_path), options.package_list_file))

        initrd_pkg_list_file = os.path.join(options.generated_data_path, 'packages_installer_initrd_expanded.json')
        initrd_pkgs = " ".join(Utils.jsonread(initrd_pkg_list_file)["packages"])

        retval = subprocess.call([script_directory + '/iso/mk-install-iso.sh',
                                  options.installer_path,
                                  working_directory, options.iso_path,
                                  options.rpm_path, options.package_list_file,
                                  rpm_list, options.stage_path, files_to_copy,
                                  options.generated_data_path, initrd_pkgs,
                                  options.photon_docker_image])

        if retval:
            raise Exception("Unable to create install ISO")

    if options.debug_iso_path:
        debug_rpm_list = create_rpm_list_to_be_copied_to_iso(
            options.pkg_to_rpm_map_file, options.pkg_to_be_copied_conf_file, 2,
            options.generated_data_path)
        make_debug_iso(working_directory, options.debug_iso_path, debug_rpm_list)

    if options.src_iso_path:
        rpm_list = create_rpm_list_to_be_copied_to_iso(options.pkg_to_rpm_map_file,
                                                       options.pkg_to_be_copied_conf_file, 4,
                                                       options.generated_data_path)
        make_src_iso(working_directory, options.src_iso_path, rpm_list)
    if os.path.exists(working_directory) and os.path.isdir(working_directory):
        shutil.rmtree(working_directory)

def replaceScript(script_dir, img, script_name, parent_script_dir=None):
    if not parent_script_dir:
        parent_script_dir = script_dir
    script = parent_script_dir + '/' + script_name
    if os.path.isfile(script_dir + '/' + img + '/' + script_name):
        script = script_dir + '/' + img + '/' + script_name
    return script

def verifyImageTypeAndConfig(config_file, img_name):
    # All of the below combinations are supported
    # 1. make image IMG_NAME=<name>
    # 2. make image IMG_NAME=<name> CONFIG=<config_file_path>
    # 3. make image CONFIG=<config_file_path>
    config = None
    if img_name and img_name != '':
        # Verify there is a directory corresponding to image
        if img_name not in next(os.walk(os.path.dirname(__file__)))[1]:
            return (False, config)
        if config_file and config_file != '' and os.path.isfile(config_file):
            config = Utils.jsonread(config_file)
            if 'image_type' in config and config['image_type'] != img_name:
                return (False, config)
        else:
            config_file = os.path.join(os.path.dirname(__file__), img_name, "config_" + img_name + ".json")
            if os.path.isfile(config_file):
                config = Utils.jsonread(config_file)
                if 'image_type' not in config:
                    config['image_type'] = img_name
            else:
                return (False, config)
        return (True, config)
    if not config_file or config_file == '':
        return (False, config)

    config = Utils.jsonread(config_file)
    return ('image_type' in config, config)

# Detach loop device and remove raw image
def cleanup(loop_devices, raw_image):
    for i,loop_dev in enumerate(loop_devices):
        Utils.runshellcommand("losetup -d {}".format(loop_dev))
        os.remove(raw_image[i])

def createImage(options):
    (validImage, config) = verifyImageTypeAndConfig(options.config_file, options.img_name)
    if not validImage:
        raise Exception("Image type/config not supported")

    if 'ova' in config['artifacttype'] and shutil.which("ovftool") is None:
        raise Exception("ovftool is not available")

    install_config = config['installer']

    image_type = config['image_type']
    image_name = config.get('image_name', 'photon-' + image_type)
    workingDir = os.path.abspath(options.stage_path + "/" + image_type)
    if os.path.exists(workingDir) and os.path.isdir(workingDir):
        shutil.rmtree(workingDir)
    os.mkdir(workingDir)
    script_dir = os.path.dirname(os.path.realpath(__file__))

    grub_script = replaceScript(script_dir, image_type, "mk-setup-grub.sh", options.installer_path)
    install_config['setup_grub_script'] = grub_script

    # Set absolute path for 'packagelist_file'
    if 'packagelist_file' in install_config:
        plf = install_config['packagelist_file']
        if not plf.startswith('/'):
            plf = os.path.join(options.generated_data_path, plf)
        install_config['packagelist_file'] = plf

    os.chdir(workingDir)

    if 'log_level' not in install_config:
        install_config['log_level'] = options.log_level

    install_config['search_path'] = [
        os.path.abspath(os.path.join(script_dir, image_type)),
        os.path.abspath(script_dir),
    ]

    # if 'photon_docker_image' is defined in config_<img>.json then ignore
    # commandline param 'PHOTON_DOCKER_IMAGE' and 'config.json' value
    if 'photon_docker_image' not in install_config:
        install_config['photon_docker_image'] = options.photon_docker_image

    if 'size' in config and 'disks' in config:
        raise Exception("Both 'size' and 'disks' key should not be defined together.Please use 'disks' for defining multidisks only.")
    elif 'size' in config:
        # 'BOOTDISK' key name doesn't matter. It is just a name given for better understanding
        config['disks'] = {"BOOTDISK": config['size']}
    elif 'disks' not in config:
        raise Exception("Disk size not defined!!")

    image_file = []
    loop_device = {}
    # Create disk image
    for ndisk, k in enumerate(config['disks']):
        image_file.append(workingDir + "/" + image_name + "-" + str(ndisk) + ".raw")
        Utils.runshellcommand(
            "dd if=/dev/zero of={} bs=1024 seek={} count=0".format(image_file[ndisk], config['disks'].get(k) * 1024))
        Utils.runshellcommand(
            "chmod 755 {}".format(image_file[ndisk]))
        # Associating loopdevice to raw disk and save the name as a target's 'disk'
        loop_device[k] = (Utils.runshellcommand(
            "losetup --show -f {}".format(image_file[ndisk]))).rstrip('\n')

    # Assigning first loop device as BOOTDISK
    install_config['disk'] = loop_device[next(iter(loop_device))]

    # Mapping the given disks to the partition table disk
    # Assigning the appropriate loop device to the partition 'disk'
    if 'partitions' in install_config:
        for partition in install_config['partitions']:
            if len(loop_device) == 1:
                partition['disk'] = install_config['disk']
            elif 'disk' in partition:
                if partition['disk'] in loop_device.keys():
                    partition['disk'] = loop_device[partition['disk']]
                else:
                    cleanup(loop_device.values(), image_file)
                    raise Exception("disk name:{} defined in partition table not found in list of 'disks'!!".format(partition['disk']))
            else:
                cleanup(loop_device.values(), image_file)
                raise Exception("disk name must be defined in partition table for multidisks!!")

    # No return value, it throws exception on error.
    runInstaller(options, install_config, workingDir)

    # Detaching loop device from vmdk
    for loop_dev in loop_device.values():
        Utils.runshellcommand("losetup -d {}".format(loop_dev))

    os.chdir(script_dir)
    imagegenerator.createOutputArtifact(
                                image_file,
                                config,
                                options.src_root,
                                options.src_root + '/tools/bin/'
                              )

if __name__ == '__main__':
    parser = ArgumentParser()

    # Common args
    parser.add_argument("-e", "--src-root", dest="src_root", default="../..")
    parser.add_argument("-f", "--installer-path", dest="installer_path", default="../../installer")
    parser.add_argument("-g", "--generated-data-path", dest="generated_data_path", default="../../stage/common/data")
    parser.add_argument("-s", "--stage-path", dest="stage_path", default="../../stage")
    parser.add_argument("-l", "--log-path", dest="log_path", default="../../stage/LOGS")
    parser.add_argument("-y", "--log-level", dest="log_level", default="debug")
    # Image builder args for ami, gce, azure, ova, rpi3 etc.
    parser.add_argument("-c", "--config-file", dest="config_file")
    parser.add_argument("-i", "--img-name", dest="img_name")
    # ISO builder args
    parser.add_argument("-j", "--iso-path", dest="iso_path")
    parser.add_argument("-k", "--debug-iso-path", dest="debug_iso_path")
    parser.add_argument("-m", "--src-iso-path", dest="src_iso_path")
    parser.add_argument("-r", "--rpm-path", dest="rpm_path", default="../../stage/RPMS")
    parser.add_argument("-x", "--srpm-path", dest="srpm_path", default="../../stage/SRPMS")
    parser.add_argument("-p", "--package-list-file", dest="package_list_file", default="../../common/data/build_install_options_all.json")
    parser.add_argument("-d", "--pkg-to-rpm-map-file", dest="pkg_to_rpm_map_file", default="../../stage/pkg_info.json")
    parser.add_argument("-z", "--pkg-to-be-copied-conf-file", dest="pkg_to_be_copied_conf_file")
    parser.add_argument("-q", "--photon-docker-image", dest="photon_docker_image", default="photon:latest")

    options = parser.parse_args()
    if options.config_file and options.config_file != '':
        options.config_file = os.path.abspath(options.config_file)
    # Create ISO
    os.chdir(os.path.dirname(os.path.realpath(__file__)))
    if options.iso_path or options.debug_iso_path or options.src_iso_path:
        createIso(options)
    elif options.config_file or options.img_name:
        createImage(options)
    else:
        raise Exception("No supported image type defined")