#!/usr/bin/env python

import argparse
import errno
import fcntl
import hashlib
import os
import re
import shutil
import subprocess
import sys
import time

parser = argparse.ArgumentParser()
parser.add_argument("-a", "--apps", help ="All external paths of application directory", type = str, default = "",nargs = argparse.REMAINDER)
parser.add_argument("-b", "--builddir", help ="Path of build directory (defaults to 'build')", type = str, default = "build")
parser.add_argument("-c", "--command", help = "Run build command directly and exit the builder", type = str, default = "", nargs = argparse.REMAINDER)
parser.add_argument("-d", "--downloads", help = "Path stored with the downloads, will be mounted as a volume", type = str, default = "")
parser.add_argument("-k", "--kernel", help = " Path of the kernel source", type = str, default = "")
parser.add_argument("-l", "--kernel_opts", help = "External path of the linux source", type = str, default = "")
parser.add_argument("-s", "--sourcedir", help ="Path of source directory (defaults to 'sources')", type = str, default = "sources")
parser.add_argument("-u", "--uboot", help = " Path of the uboot source", type = str, default = "")
parser.add_argument("-v", "--uboot_opts", help = "External path of the  bootloader source", type = str, default = "")
parser.add_argument("-t", "--toolschain", help = " Path of the toolschain source", type = str, default = "")
parser.add_argument("-p", "--toolschain_opts", help = "External path of the toolschain source", type = str, default = "")

args = parser.parse_args()

class BaseConfig(object):
    class Logger(object):
        DEBUG, NOTE, WARNING, ERROR, CRITICAL = list(range(0,5))
        BASECOLOR, BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = list(range(29,38))
        LEVELNAMES = {
            DEBUG   : "DEBUG",
            NOTE    : "NOTE",
            WARNING : "WARNING",
            ERROR   : "ERROR",
            CRITICAL: "CRITICAL",
        }

        COLORS = {
            DEBUG   : CYAN,
            NOTE    : BASECOLOR,
            WARNING : YELLOW,
            ERROR   : RED,
            CRITICAL: RED,
        }

        BLD = '\033[1;%dm'
        STD = '\033[%dm'
        RST = '\033[0m'

        colored = True

        def disable_color(self):
            self.colored = False

        def colorize(self, levelno, msg):
            color = self.COLORS[levelno]
            levelname = self.LEVELNAMES[levelno]
            if self.colored and color is not None and levelname is not None:
                level = "".join([self.BLD % color, levelname, self.RST])
                msg = "".join([self.STD % color, msg, self.RST])
                return "%s: %s" % (level, msg)
            return msg

        def debug(self, msg):
            print(self.colorize(self.DEBUG, msg))

        def note(self, msg):
            print(self.colorize(self.NOTE, msg))

        def warn(self, msg):
            print(self.colorize(self.WARNING, msg))

        def error(self, msg):
            print(self.colorize(self.ERROR, msg))

        def critical(self, msg):
            print(self.colorize(self.CRITICAL, msg))

    logger = Logger()

    def __init__(self):
        self.uid = os.getuid()
        self.gid = 27

        # Any change in the listed items will triger a rebuild
        self.checkfilelist = "home Dockerfile start.sh"
        self.checkvarslist = "sourcedir"

    def _mkdirhier(self, directory):
        """
        Create a directory like 'mkdir -p', but does not complain if
        directory already exists like os.makedirs
        """

        try:
            os.makedirs(directory)
        except OSError as e:
            if e.errno != errno.EEXIST:
                raise e

    def _is_file_newer(self, file, timestamp):
        """
        Check if the file modify time newer than the timestamp
        """

        return True if os.stat(file).st_mtime > timestamp else False

    def check_vars(self):
        """
        Check system arguments passed to this script, and set related
        variables according to how it's being called
        """

        if self.uid == 0:
            self.logger.error("setup script can not be run by 'root' user.")
            exit(1)

        self.builddir = os.path.join("/opt/uml/",args.builddir)
        self.sourcedir = os.path.join("/opt/uml/",args.sourcedir)
        
        self.kernel_opts = ""
        self.kernelurl = ""
        if args.kernel_opts:
            self.kernel_opts = "-v %s:/opt/uml/externalsrc/kernel-src" % args.kernel_opts
            self.kernelurl = self.kernel_opts.split(':')[1]
        elif args.kernel:
            if not os.path.basename(args.kernel):
                args.kernel = args.kernel.split('/')[-2]
            self.kernelurl = "/".join([self.sourcedir,os.path.basename(args.kernel)])
        else: 
            self.kernelurl = "git@bitbucket.org:bcld_a20/kernel-3.4.61.git;branch=a20_dev;"
        
        self.uboot_opts = ""
        self.ubooturl = ""
        if args.uboot_opts:
            self.uboot_opts = "-v %s:/opt/uml/externalsrc/uboot-src" %  args.uboot_opts
            self.ubooturl = self.uboot_opts.split(':')[1]
        elif args.uboot:
            if not os.path.basename(args.uboot):
                args.uboot = args.uboot.split('/')[-2]
            self.ubooturl = "/".join([self.sourcedir,os.path.basename(args.uboot)]) 
        else:
            self.ubooturl = "git@bitbucket.org:bcld_a20/a20_uboot.git;branch=master"

        self.toolschain_opts = ""
        self.toolschainurl = ""
        if args.toolschain_opts:
            self.toolschain_opts = "-v %s:/opt/uml/toolschain/external" %  args.toolschain_opts
            self.toolschainurl = "/opt/uml/toolschain/external"
        elif args.toolschain:
            if not os.path.basename(args.toolschain):
                args.toolschain = args.toolschain.split('/')[-2]
            self.toolschainurl = "/".join(["/opt/uml/toolschain",os.path.basename(args.toolschain)])
        else:
            self.logger.error("Please Set the toolschain path first using -t or -p option")
            parser.print_help()
            exit(1)


        self.command = args.command
        self.apps = []
        for app in args.apps:
            app_name = app
            if not os.path.basename(app_name):
                app_name = app_name.split('/')[-2]
            app_name = "-v %s:/opt/uml/build/%s" %(app,os.path.basename(app_name))
            self.apps.append(app_name)
    
    def change_cwd(self):
        """
        Ensure the current working directory is same with the
        directory where this script is locating in, and the script
        itself should be a symbolic link to docker/setup.
        """
        try:
            if os.readlink(__file__) != os.path.join("docker", "setup"):
                raise OSError("The link target does not exist.")
        except OSError:
            self.logger.error("setup script must be run in the top directory. (not in this docker directory)")
            exit(1)

        execdir = os.path.dirname(os.path.abspath(__file__))
        
        if os.getcwd() != execdir:
            self.logger.note("Changing cwd to %s" % execdir)
            os.chdir(execdir)
            self._mkdirhier("externalsrc")
            self._mkdirhier("toolschain/external/")


    def check_docker(self):
        """
        Check if docker binary exists in PATH and its version
        is higher than 1.0
        """

        # Check docker binary
        dockerbin=""
        for p in os.getenv('PATH').split(':'):
            candidate = os.path.join(p, 'docker')
            if os.path.isfile(candidate) and os.access(candidate, os.X_OK):
                dockerbin = candidate
                if not os.path.isabs(dockerbin):
                    dockerbin = os.path.abspath(dockerbin)
                # Found the first executable docker
                break
        if not dockerbin:
            self.logger.error("Can not find docker in PATH. you must have docker installed!")
            exit(1)

        # Check docker version
        dockerver = subprocess.check_output("%s --version" % dockerbin, shell = True)
        dockerver = re.match("^.*([0-9]+\.[0-9]+\.[0-9]+).*$", dockerver).group(1)
        if int(dockerver.split('.')[0]) < 1:
            self.logger.error("docker version must be higher than 1.0, current is %s" % dockerver)
            exit(1)

    def build_image(self):
        """
        Build builder image if it does not exist, or compare its
        '.Created' information with the 'Modify' timestamp of all files
        in checkfilelist, and rebuild the image if any of the timestamp
        is later than image 'Created' time.
        """

        rebuild = False
        nocache = "false"
        try:
            output = subprocess.check_output("docker inspect --format={{.Created}} builder:uml 2>/dev/null", shell = True)
            m = re.match(r'(^[0-9]{4}-[0-9]{2}-[0-9]{2})[a-zA-Z ]{1}([0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]{6}).*$', output)
            created = time.mktime(time.strptime('%s %s' % (m.group(1), m.group(2)), '%Y-%m-%d %H:%M:%S.%f'))

            # Check file 'Modify' timestamp of checkfilelist
            for l in self.checkfilelist.split():
                p = "docker/%s" % l
                if os.path.isdir(p):
                    for root, _, files in os.walk(p):
                        for f in files:
                            file = os.path.join(root, f)
                            if self._is_file_newer(file, created):
                                rebuild = True
                                break
                elif os.path.isfile(p):
                    if self._is_file_newer(p, created):
                        rebuild = True
                        break

            # Check variable changes of checkvarslist
            data = ""
            for v in self.checkvarslist.split():
                data += str(eval("self.%s" % v)).strip()
            datahash = hashlib.md5(data.encode("utf-8")).hexdigest()
            try:
                if open(".sigdata", 'r').read() != datahash:
                    rebuild = True
                    nocache = "true"
            except IOError:
                rebuild = True
                nocache = "true"
            finally:
                open(".sigdata", 'w').write(datahash)

        except subprocess.CalledProcessError:
            rebuild = True

        if rebuild:
            cmd = "cd docker; docker build --no-cache=%s -t builder:uml ./" % nocache
            self.logger.note("Building docker builder image... (This may take some time.)")
            subprocess.check_output(cmd, shell = True)

    def start_image(self):
        """
        Start the builder image in docker
        """

        psedottyargs = "" if self.command else "-t"
        cmd = "docker run --privileged=true --rm -h Ubuntu-Xenial -e DISPLAY=:0 -i %s \
            -v %s/.ssh:/home/builder/.ssh \
            -v %s/.ssh:/root/.ssh \
            -v /etc/localtime:/etc/localtime \
            -v /tmp/.X11-unix:/tmp/.X11-unix \
            -v /dev:/dev \
            %s \
            -v %s:/opt/uml %s %s %s  builder:uml %s %s \"%s\" \"%s\" \"%s\" \"%s\" \"%s\" \"%s\"" \
            % (psedottyargs, os.getenv("HOME"),os.getenv("HOME"), " ".join(self.apps),os.getcwd(), self.kernel_opts, self.uboot_opts,self.toolschain_opts, \
               self.uid, self.gid, self.builddir,self.sourcedir, self.kernelurl, self.ubooturl,self.toolschainurl," ".join(self.command))
        self.logger.note("Running build machine...")
        return subprocess.call(cmd, shell = True)

    def setup(self):
        self.check_vars()
        self.change_cwd()
        self.check_docker()
        self.build_image()
        return self.start_image()

if __name__ == "__main__":
    try:
        config = BaseConfig()
        ret = config.setup()
    except Exception as esc:
        ret = 1
        import traceback
        traceback.print_exc()
    sys.exit(ret)
