#!/usr/bin/env python3

# Copyright (C) 2022  Braiins Systems s.r.o.
#
# This file is part of Braiins Open-Source Initiative (BOSI).
#
# BOSI is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <https://www.gnu.org/licenses/>.
#
# Please, keep in mind that we may also license BOSI or any part thereof
# under a proprietary license. For more information on the terms and conditions
# of such proprietary license or if you have any other questions, please
# contact us at opensource@braiins.com.

# SD partition info
#
#  First partition
#    fs: FAT16
#    size: 41943040 (40 MiB)
#    label: BOS
#  Second partition
#    fs: Ext4
#    size: 31457280 (30 MiB)
#    label: overlay

import argparse
import string
import logging as log
import tempfile
import glob
import subprocess
import shutil
import distutils.dir_util as directory_utils
import os
import pathlib


class SdBuild:
    FIRST_PARTITION_SIZE_MB = 40
    FIRST_PARTITION_LABEL = 'BOS'

    SECOND_PARTITION_SIZE_MB = 30
    SECOND_PARTITION_LABEL = 'overlay'

    def __init__(
        self,
        first_partition_data_dir: string,
        second_partition_data_dir: string,
        output_image: string,
    ):
        """
        Constructs SdBuild with all the necessary attributes.

        Parameters
        ----------
            first_partition_data_dir : str
                Path to directory with data for first partition
            second_partition_data_dir : str
                Path to directory with data for second partition
            output_image : str
                Final image path
        """
        self._first_partition_data_dir = first_partition_data_dir
        self._second_partition_data_dir = second_partition_data_dir
        self._output_image = output_image
        self._validate()

    def _validate(self):
        import pathlib

        # Validate first partition source data directory
        path = pathlib.Path(self._first_partition_data_dir)
        if not path.exists():
            raise ValueError('{} does not exist!'.format(path))
        if not path.is_dir():
            raise ValueError('{} is not directory'.format(path))
        if not path.parts:
            raise ValueError('{} is empty directory'.format(path))

        # Validate second partition source data directory
        path = pathlib.Path(self._second_partition_data_dir)
        if not path.exists():
            raise ValueError('{} does not exist!'.format(path))
        if not path.is_dir():
            raise ValueError('{} is not directory'.format(path))

        # Validate SD image target directory
        path = pathlib.Path(self._output_image)
        if path.is_dir():
            raise ValueError('{} is directory'.format(self._output_image))
        if not path.parent.exists():
            raise ValueError('target directory {} does not exist'.format(path.parent))

    def build_image(self):
        log.info('Building new SD image %s', self._output_image)
        with tempfile.TemporaryDirectory() as tmpdir:
            log.info('Created temporary directory %s', tmpdir)
            img1 = self._prepare_first_partition(tmpdir)
            img2 = self._prepare_second_partition(tmpdir)
            self._prepare_final_image(tmpdir, img1, img2)

    def _prepare_first_partition(self, tmpdir):
        """
        Prepare first partition image.

        Prepare FAT image in prepared temporary storage
        with data copied from provided `self._first_partition_data_dir`.
        Image has size equal to FIRST_PARTITION_SIZE_MB nad label FIRST_PARTITION_LABEL

        Parameters
        ----------
        tmpdir : str
            Path to temporary directory where partition 1 image will be temporary stored

        Returns
        -------
        Path to the first partition image
        """
        log.info('Preparing first partition')
        partition_1_img = os.path.join(tmpdir, 'part1.img')  # $tmpdir/part1.img
        partition_file_paths = glob.glob('{}/*'.format(self._first_partition_data_dir))

        # dd if=/dev/zero of=$tmpdir/part1.img bs=1M count=$FIRST_PARTITION_SIZE_MB
        subprocess.run(
            [
                'dd',
                'if=/dev/zero',
                'of={}'.format(partition_1_img),
                'bs=1M',
                'count={}'.format(self.FIRST_PARTITION_SIZE_MB),
            ],
            check=True,
        )

        # mkfs.vfat $tmpdir/part1.img -n FIRST_PARTITION_LABEL
        subprocess.run(
            ['/sbin/mkfs.vfat', partition_1_img, '-n', self.FIRST_PARTITION_LABEL],
            check=True,
        )

        # mcopy -i $tmpdir/part1.img $src/* ::/
        subprocess.run(
            ['mcopy', '-i', partition_1_img, *partition_file_paths, '::/'], check=True
        )

        log.info('First partition image %s prepared', partition_1_img)
        return partition_1_img

    def _prepare_second_partition(self, tmpdir):
        """
        Prepare second partition image.

        Prepare EXT4 image in prepared temporary storage
        with data copied from provided `self._second_partition_data_dir`.
        Image has size equal to SECOND_PARTITION_SIZE_MB nad label SECOND_PARTITION_LABEL

        Parameters
        ----------
        tmpdir : str
            Path to temporary directory where partition 2 image will be temporary stored

        Returns
        -------
        Path to the second partition image
        """
        log.info('Preparing second partition')
        partition_2_img = os.path.join(tmpdir, 'part2.img')  # $tmpdir/part1.img

        partition_2_dir = os.path.join(tmpdir, 'part2')  # $tmpdir/part2
        os.mkdir(partition_2_dir)

        # dd if=/dev/zero of=$tmpdir/part2.img bs=1M count=SECOND_PARTITION_SIZE_MB
        subprocess.run(
            [
                'dd',
                'if=/dev/zero',
                'of={}'.format(partition_2_img),
                'bs=1M',
                'count={}'.format(self.SECOND_PARTITION_SIZE_MB),
            ],
            check=True,
        )

        directory_utils.copy_tree(self._second_partition_data_dir, partition_2_dir)

        # fakeroot mkfs.ext4 $tmpdir/part2.img -d $tmpdir/part2 -L SECOND_PARTITION_LABEL
        subprocess.run(
            [
                'fakeroot',
                '/sbin/mkfs.ext4',
                partition_2_img,
                '-d',
                partition_2_dir,
                '-L',
                self.SECOND_PARTITION_LABEL,
            ],
            check=True,
        )

        log.info('Second partition image %s prepared', partition_2_img)
        return partition_2_img

    def _prepare_final_image(self, tmpdir, first_image_path, second_image_path):
        """
        Prepare final SD image.

        # Assemble the whole image from prepared partition images

        Parameters
        ----------
        tmpdir : str
            Path to temporary directory
        first_image_path : str
            First partition image path
        second_image_path : str
            Second partition image path

        Returns
        -------
        None
        """
        log.info('Building final SD image %s', self._output_image)

        # dd if=/dev/zero of=$tmpdir/part.img bs=1M count=$((1 + FIRST_PARTITION_SIZE_MB + SECOND_PARTITION_SIZE_MB))
        subprocess.run(
            [
                'dd',
                'if=/dev/zero',
                'of={}/part.img'.format(tmpdir),
                'bs=1M',
                'count={}'.format(self._get_final_img_size()),
            ],
            check=True,
        )

        # sfdisk $tmpdir/part.img <<-EOF
        #     start=1M, size=FIRST_PARTITION_SIZE_MBM, type=c
        #     size=SECOND_PARTITION_SIZE_MBM, type=83
        # EOF
        popen = subprocess.Popen(
            ['/sbin/sfdisk', '{}/part.img'.format(tmpdir)],
            stdin=subprocess.PIPE,
            stdout=subprocess.PIPE,
            stderr=subprocess.STDOUT,
        )
        popen.stdin.write(
            """start=1M, size={0}M, type=c
            size={1}M, type=83""".format(
                self.FIRST_PARTITION_SIZE_MB, self.SECOND_PARTITION_SIZE_MB
            ).encode()
        )
        popen.stdin.close()

        # dd if=$tmpdir/part1.img of=$tmpdir/part.img bs=1M seek=1 count=FIRST_PARTITION_SIZE_MB conv=notrunc
        subprocess.run(
            [
                'dd',
                'if={}'.format(first_image_path),
                'of={}/part.img'.format(tmpdir),
                'bs=1M',
                'seek=1',
                'count={}'.format(self.FIRST_PARTITION_SIZE_MB),
                'conv=notrunc',
            ],
            check=True,
        )

        # dd if=$tmpdir/part2.img of=$tmpdir/part.img bs=1M seek=$((1 + FIRST_PARTITION_SIZE_MB)) count=SECOND_PARTITION_SIZE_MB conv=notrunc
        subprocess.run(
            [
                'dd',
                'if={}'.format(second_image_path),
                'of={}/part.img'.format(tmpdir),
                'bs=1M',
                'seek={}'.format(1 + self.FIRST_PARTITION_SIZE_MB),
                'count={}'.format(self.SECOND_PARTITION_SIZE_MB),
                'conv=notrunc',
            ],
            check=True,
        )

        log.info('SD image prepared in temp directory. Copying to target directory...')

        # cp $tmpdir/part.img dst
        shutil.copy2('{}/part.img'.format(tmpdir), self._output_image)

        log.info('SD image %s prepared', self._output_image)

    def _get_final_img_size(self):
        return 1 + self.FIRST_PARTITION_SIZE_MB + self.SECOND_PARTITION_SIZE_MB


def main():
    log.basicConfig(level=log.INFO)

    parser = argparse.ArgumentParser(
        description='This script is used to automate disk image creation'
    )
    parser.add_argument(
        'first_partition',
        type=pathlib.Path,
        help='Path to directory with content of the first partition',
    )
    parser.add_argument(
        'second_partition',
        type=pathlib.Path,
        help='Path to directory with content of the second partition',
    )
    parser.add_argument('image', type=pathlib.Path, help='Path to output image')

    args = parser.parse_args()

    first_partition = str(args.first_partition)
    second_partition = str(args.second_partition)
    image = str(args.image)
    SdBuild(first_partition, second_partition, image).build_image()


if __name__ == '__main__':
    main()
