#!/usr/bin/env python3

import argparse
import os
import subprocess
import sys
import time

SRC_MOUNT = "/root/src"
STAGING_MOUNT = "/root/.floss"


class FlossContainerRunner:
    """Runs Floss build inside container."""

    # Commands to run for build
    BUILD_COMMANDS = [
        # First run bootstrap to get latest code + create symlinks
        [f'{SRC_MOUNT}/build.py', '--run-bootstrap', '--clone-timeout=1200'],

        # Clean up any previous artifacts inside the volume
        [f'{SRC_MOUNT}/build.py', '--target', 'clean'],

        # Run normal code builder
        [f'{SRC_MOUNT}/build.py', '--target', 'all'],

        # Run tests
        [f'{SRC_MOUNT}/build.py', '--target', 'test'],
    ]

    def __init__(self, workdir, rootdir, image_tag, volume_name, container_name, staging_dir, use_docker,
                 use_pseudo_tty):
        """ Constructor.

        Args:
            workdir: Current working directory (should be the script path).
            rootdir: Root directory for Bluetooth.
            image_tag: Tag for container image used for building.
            volume_name: Volume name used for storing artifacts.
            container_name: Name for running container instance.
            staging_dir: Directory to mount for artifacts instead of using volume.
            use_docker: Use docker binary if True (or podman when False).
            use_pseudo_tty: Run container with pseudo tty if true.
        """
        self.workdir = workdir
        self.rootdir = rootdir
        self.image_tag = image_tag
        self.container_binary = 'docker' if use_docker else 'podman'
        self.env = os.environ.copy()

        # Flags used by container exec:
        # -i: interactive mode keeps STDIN open even if not attached
        # -t: Allocate a pseudo-TTY (better user experience)
        #     Only set if use_pseudo_tty is true.
        self.container_exec_flags = '-it' if use_pseudo_tty else '-i'

        # Name of running container
        self.container_name = container_name

        # Name of volume to write output.
        self.volume_name = volume_name
        # Staging dir where we send output instead of the volume.
        self.staging_dir = staging_dir

    def run_command(self, target, args, cwd=None, env=None, ignore_rc=False):
        """ Run command and stream the output.
        """
        # Set some defaults
        if not cwd:
            cwd = self.workdir
        if not env:
            env = self.env

        rc = 0
        process = subprocess.Popen(args, cwd=cwd, env=env, stdout=subprocess.PIPE)
        while True:
            line = process.stdout.readline()
            print(line.decode('utf-8'), end="")
            if not line:
                rc = process.poll()
                if rc is not None:
                    break

                time.sleep(0.1)

        if rc != 0 and not ignore_rc:
            raise Exception("{} failed. Return code is {}".format(target, rc))

    def _create_volume_if_needed(self):
        # Check if the volume exists. Otherwise create it.
        try:
            subprocess.check_output([self.container_binary, 'volume', 'inspect', self.volume_name])
        except:
            self.run_command(self.container_binary + ' volume create',
                             [self.container_binary, 'volume', 'create', self.volume_name])

    def start_container(self):
        """Starts the container with correct mounts."""
        # Stop any previously started container.
        self.stop_container(ignore_error=True)

        # Create volume and create mount string
        if self.staging_dir:
            mount_output_volume = 'type=bind,src={},dst={}'.format(self.staging_dir, STAGING_MOUNT)
        else:
            # If not using staging dir, use the volume instead
            self._create_volume_if_needed()
            mount_output_volume = 'type=volume,src={},dst={}'.format(self.volume_name, STAGING_MOUNT)

        # Mount the source directory
        mount_src_dir = 'type=bind,src={},dst={}'.format(self.rootdir, SRC_MOUNT)

        # Run the container image. It will run `tail` indefinitely so the container
        # doesn't close and we can run `<container_binary> exec` on it.
        self.run_command(self.container_binary + ' run', [
            self.container_binary, 'run', '--name', self.container_name, '--mount', mount_output_volume, '--mount',
            mount_src_dir, '-d', self.image_tag, 'tail', '-f', '/dev/null'
        ])

    def stop_container(self, ignore_error=False):
        """Stops the container for build."""
        self.run_command(self.container_binary + ' stop',
                         [self.container_binary, 'stop', '-t', '1', self.container_name],
                         ignore_rc=ignore_error)
        self.run_command(self.container_binary + ' rm', [self.container_binary, 'rm', self.container_name],
                         ignore_rc=ignore_error)

    def do_build(self):
        """Runs the basic build commands."""
        # Start container before building
        self.start_container()

        try:
            # Run all commands
            for i, cmd in enumerate(self.BUILD_COMMANDS):
                self.run_command(self.container_binary + ' exec #{}'.format(i),
                                 [self.container_binary, 'exec', self.container_exec_flags, self.container_name] + cmd)
        finally:
            # Always stop container before exiting
            self.stop_container()

    def print_do_build(self):
        """Prints the commands for building."""
        container_exec = [self.container_binary, 'exec', self.container_exec_flags, self.container_name]
        print('Normally, build would run the following commands: \n')
        for cmd in self.BUILD_COMMANDS:
            print(' '.join(container_exec + cmd))

    def check_container_runnable(self):
        try:
            subprocess.check_output([self.container_binary, 'ps'], stderr=subprocess.STDOUT)
        except subprocess.CalledProcessError as err:
            if 'denied' in err.output.decode('utf-8'):
                print('Run script as sudo')
            else:
                print('Unexpected error: {}'.format(err.output.decode('utf-8')))

            return False

        # No exception means container is ok
        return True


if __name__ == "__main__":
    parser = argparse.ArgumentParser('Builder Floss inside container image.')
    parser.add_argument('--only-start',
                        action='store_true',
                        default=False,
                        help='Only start the container. Prints the commands it would have ran.')
    parser.add_argument('--only-stop', action='store_true', default=False, help='Only stop the container and exit.')
    parser.add_argument('--image-tag', default='floss:latest', help='Container image to use to build.')
    parser.add_argument('--volume-tag',
                        default='floss-out',
                        help='Name of volume to use. This is where build artifacts will be stored by default.')
    parser.add_argument('--staging-dir',
                        default=None,
                        help='Staging directory to use instead of volume. Build artifacts will be written here.')
    parser.add_argument('--container-name',
                        default='floss-container-runner',
                        help='What to name the started container.')
    parser.add_argument('--use-docker',
                        action='store_true',
                        default=False,
                        help='Use flag to use Docker to build Floss. Defaults to using podman.')
    parser.add_argument('--no-tty',
                        action='store_true',
                        default=False,
                        help='Use flag to disable pseudo tty for docker container.')
    args = parser.parse_args()

    # cwd should be set to same directory as this script (that's where
    # Dockerfile is kept).
    workdir = os.path.dirname(os.path.abspath(sys.argv[0]))
    rootdir = os.path.abspath(os.path.join(workdir, '../..'))

    # Determine staging directory absolute path
    staging = os.path.abspath(args.staging_dir) if args.staging_dir else None

    fdr = FlossContainerRunner(workdir, rootdir, args.image_tag, args.volume_tag, args.container_name, staging,
                               args.use_docker, not args.no_tty)

    # Make sure container is runnable before continuing
    if fdr.check_container_runnable():
        # Handle some flags
        if args.only_start:
            fdr.start_container()
            fdr.print_do_build()
        elif args.only_stop:
            fdr.stop_container()
        else:
            fdr.do_build()
