#!/usr/bin/env python3

##########################################################################
# Copyright (c) 2016, 2019 ETH Zurich.
# All rights reserved.
#
# This file is distributed under the terms in the attached LICENSE file.
# If you do not find this file, copies can be found by writing to:
# ETH Zurich D-INFK, Universitaetstr 6, CH-8092 Zurich. Attn: Systems Group.
##########################################################################

import subprocess
import os, argparse, re

class EFIImage:

    def __init__(self, image, size):
        """
        Size in MiB
        """
        self._image = image
        # Size of the disk
        self._sizeMB= size * 1024 * 1024
        # block size
        self._blockSize = 512
        # size of the disk in blocks
        self._sizeBlocks= self._sizeMB // self._blockSize
        # first block of partition
        self._startBlock = 2048

        # size of partition in blocks
        self._partSizeBlocks = self._sizeBlocks - self._startBlock

        # calculate byte offset to first partition and format as mformat name
        self._mformatImage="%s@@%d" % (self._image, self._startBlock*self._blockSize)

        self._dirs = None

    def _cmd(self, command, **kwargs):
        print(" ".join(command))
        return subprocess.check_call(command, **kwargs)


    def create(self):
        self._cmd(["dd", "if=/dev/zero", "of=%s" % self._image,
                  "bs=%d" % self._blockSize, "count=1",
                  "seek=%d" % (self._sizeBlocks - 1)])

        self._cmd(["/sbin/parted", "-s", self._image, "mktable", "gpt"])
        self._cmd(["/sbin/parted", "-s", self._image, "mkpart", "primary", "fat32",
                  "%ds" % self._startBlock, "%ds" % self._partSizeBlocks])
        self._cmd(["/sbin/parted", "-s", self._image, "align-check", "optimal", "1"])
        self._cmd(["/sbin/parted", "-s", self._image, "name", "1", "UEFI"])

        self._cmd(["mformat", "-i", self._mformatImage, "-T",
                  str(self._partSizeBlocks), "-h", "1", "-s", "1"])
        # mdir fails if the root directory is empty. We create a directory here
        # to make sure _initDirCache does not fail.
        self._cmd(["mmd", "-i", self._mformatImage, "dummy"])

        # reset directory cache
        self._dirs = None

    def _initDirCache(self):
        if not self._dirs is None:
            return
        self._dirs = set()
        cmd = ["mdir", "-i", self._mformatImage, "-/b"]
        print(" ".join(cmd))
        proc = subprocess.Popen(cmd, stdout=subprocess.PIPE)
        for line in proc.stdout:
            if line.endswith(b"/"):
                self._dirs.add(line[:-1])

    def _createParentDir(self, dirName):
        """ Create a parent directory for passed directory name """
        parentDir = os.path.dirname(dirName)
        if parentDir is "":
            return "::"
        basename = "::/%s" % parentDir
        if not basename in self._dirs:
            self._createParentDir(parentDir)
            self._cmd(["mmd", "-i", self._mformatImage, basename])
            self._dirs.add(basename)
        return basename

    def addFile(self, inFile, fileName):
        if self._dirs is None:
            self._initDirCache()
        dirName = self._createParentDir(fileName)
        targetFile = os.path.join(dirName, os.path.basename(fileName))
        self._cmd(["mcopy", "-o", "-s", "-i", self._mformatImage, inFile, targetFile])

    def writeFile(self, fileName, contents):
        if self._dirs is None:
            self._initDirCache()
        dirName = self._createParentDir(fileName)
        cmd = ["mcopy", "-o", "-s", "-i", self._mformatImage, "-", os.path.join(dirName, os.path.basename(fileName))]
        print(" ".join(cmd))
        proc = subprocess.Popen(cmd, stdin=subprocess.PIPE)
        proc.communicate(bytes(contents, 'utf-8'))
        proc.stdin.close()


#
# Command line and interface functions
#

def build_bf_efi_img(img_file, build_dir, modules, menulst, hagfish):
    hagfish_default = os.path.abspath(os.path.join(
        os.path.dirname(os.path.realpath(__file__)),
        "..","hagfish","Hagfish.efi"))
    if hagfish is None:
        hagfish = hagfish_default

    efi = EFIImage(img_file, 200)
    efi.create()
    for module in modules:
        if module[0] == "/":
            module = module[1:]
        efi.addFile(os.path.join(build_dir, module), module)
    efi.writeFile("startup.nsh", "Hagfish.efi hagfish.cfg")
    efi.addFile(hagfish, "Hagfish.efi")
    efi.addFile(menulst, "hagfish.cfg")

def parse_menu_lst(menulst):
    with open(menulst) as f:
        res = []
        for line in f.readlines():
            line = line.strip()
            if line=="" or line[0] == "#": continue
            parts = re.split("\s+", line, 2)
            if len(parts) < 1:
                print("Ignoring line: %s" % line)
                continue
            if parts[0] in ["stack"]:
                # no module
                continue
            res.append(parts[1])

    return res

def parse_args():
    p = argparse.ArgumentParser(
        description='Build EFI image for ARMv8')
    p.add_argument('menulst', help='the menu.lst')
    p.add_argument('build', help='build directory')
    p.add_argument('file', help='output image')
    p.add_argument('--hagfish', help='hagfish location', default=None)
    return p.parse_args()

if __name__ == "__main__":
    print("Barrelfish EFI Image Builder")
    args = parse_args()
    print("Image File: %s" % args.file)
    print("menu.lst: %s" % args.menulst)
    modules = parse_menu_lst(args.menulst)
    print("Modules: %s" % ",".join(modules))
    build_bf_efi_img(args.file, args.build, modules, args.menulst, args.hagfish)
