#!/usr/bin/env python
# Copyright 2013 Daniel Narvaez
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#
#    http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import fcntl
import json
import os
import shutil
import subprocess
import sys
import tarfile
import urllib2
import StringIO

base_dir = os.path.dirname(os.path.abspath(__file__))
virtualenv_version = "1.9.1"
broot_dir = "build"
commands_dir = os.path.join("build", "commands")
main_repo = "sugarlabs/sugar-build"
docs_url = "http://developer.sugarlabs.org/dev-environment.md.html"
buildbot_env = "SUGAR_BUILDBOT"
lock_file = None

def get_lock_file():
    global lock_file

    if lock_file is not None:
        return lock_file

    lock_name = ".lock"
    if use_chroot():
        lock_name = ".lock-chroot"

    lock_file = open(os.path.join(get_base_dir(), lock_name), "w")

    return lock_file

def get_base_dir():
    return base_dir


def get_sandbox_dir():
    sandbox_name = "sandbox"
    if use_chroot():
        sandbox_name = "sandbox-chroot"

    return os.path.join(get_base_dir(), "build", "out", sandbox_name)


def get_broot_dir():
    return os.path.join(get_base_dir(), broot_dir)


def get_commands_dir():
    return os.path.join(get_base_dir(), commands_dir)


def get_cache_dir():
    return os.path.join(get_sandbox_dir(), "cache")


def get_virtualenv_dir():
    return os.path.join(get_sandbox_dir(), "install")


def get_prefs_path():
    return os.path.join(get_base_dir(), "prefs.json")


def get_stamp_path():
    return os.path.join(get_sandbox_dir(), "stamp")


def get_bin_path(name=None):
    bin_path = os.path.join(get_virtualenv_dir(), "bin")

    if name is None:
        return bin_path
    else:
        return os.path.join(bin_path, name)


def create_virtualenv():
    source_dir = os.path.join(get_cache_dir(),
                              "virtualenv-%s" % virtualenv_version)

    if not os.path.exists(source_dir):
        url = "https://pypi.python.org/packages/source/v/" \
              "virtualenv/virtualenv-%s.tar.gz" % virtualenv_version

        f = urllib2.urlopen(url)
        buffer = StringIO.StringIO(f.read())
        with tarfile.open(fileobj=buffer, mode="r:gz") as tar:
            tar.extractall(get_cache_dir())

    subprocess.check_call(["python", os.path.join(source_dir, "virtualenv.py"),
                           "-q", "--system-site-packages",
                           get_virtualenv_dir()])


def install_osbuild():
    subprocess.check_call([get_bin_path("pip"), "-q", "install",
                           "git+git://github.com/dnarvaez/osbuild"])


def install_broot():
    subprocess.check_call([get_bin_path("pip"), "-q", "install",
                           "git+git://github.com/dnarvaez/broot"])


def check_stamp():
    changed = []
    new_stamp = {}
    saved_stamp = {}

    stamp_path = get_stamp_path()

    try:
        os.makedirs(os.path.dirname(stamp_path))
    except OSError:
        pass

    try:
        with open(stamp_path) as f:
            saved_stamp = json.load(f)
    except (IOError, ValueError):
        pass

    repos = [main_repo]

    if use_chroot():
        repos.append("dnarvaez/broot")
    else:
        repos.append("dnarvaez/osbuild")

    for repo in repos:
        url = "https://api.github.com/repos/%s/branches/master" % repo
        headers = {}

        etag = saved_stamp.get(repo, {}).get("etag", None)
        if etag:
            headers = {"If-None-Match": etag}

        code = 200

        try:
            response = urllib2.urlopen(urllib2.Request(url, headers=headers))
        except urllib2.HTTPError, e:
            if e.code in (304, 403):
                code = e.code
            else:
                raise e

        if code == 200:
            sha = json.load(response)["commit"]["sha"]

            if saved_stamp.get(repo, {}).get("sha", None) != sha:
                changed.append(repo)

            new_stamp[repo] = {"sha": sha,
                               "etag": response.info()["ETag"]}
        elif code == 304:
            new_stamp[repo] = saved_stamp[repo]
        elif code == 403:
            print("WARNING: GitHub rate limiting exceeded")
            changed.append(repo)

    with open(stamp_path, "w") as f:
        json.dump(new_stamp, f)

    return changed


def delete_virtualenv():
    try:
        shutil.rmtree(get_virtualenv_dir())
    except OSError:
        pass


def check_binaries(binary_names):
    env = {"PATH": os.path.expandvars("/usr/sbin:/sbin:$PATH")}

    with open(os.devnull, "w") as devnull:
        for name in binary_names:
            try:
                subprocess.call([name], stdout=devnull,
                                stderr=subprocess.STDOUT, env=env)
            except OSError:
                print("Please install %s." % name)
                return False

    return True


def check_dependencies():
    if use_chroot():
        if not check_binaries(["yum", "rpm"]):
            return False
    else:
        if not check_binaries(["gcc", "pkg-config"]):
            return False

        if subprocess.call(["pkg-config", "--exists", "python-2.7"]) != 0:
            print("Please install the python development package.")
            return False

    return True


def clean():
    sandbox_dir = get_sandbox_dir()

    shutil.rmtree(sandbox_dir, ignore_errors=True)

    parent_dir = os.path.dirname(sandbox_dir)

    try:
        if not os.listdir(parent_dir):
            os.rmdir(parent_dir)
    except OSError:
        pass


def check_upgrade():
    try:
        changed = check_stamp()
    except (urllib2.HTTPError, urllib2.URLError), e:
        print ("Failed to check for changes, skipping setup. "
               "Is your network working?")
        print ("Error: %s" % e)
        changed = None

    return changed


def is_buildbot():
    return buildbot_env in os.environ


def get_chroot_prefs():
    try:
        with open(get_prefs_path()) as f:
            prefs = json.load(f)
    except IOError:
        prefs = {}

    return prefs.get("chroot", None)


def use_chroot():
    if "BROOT" in os.environ:
        return False

    return get_chroot_prefs() is not None


def get_virtualenv_exists():
    return os.path.exists(get_virtualenv_dir())


def setup(changed=[]):
    upgrade = get_virtualenv_exists()
    build_virtualenv = not upgrade

    if "dnarvaez/osbuild" in changed:
        build_virtualenv = True

    if "dnarvaez/broot" in changed:
        build_virtualenv = True

    if build_virtualenv:
        if upgrade:
            print("\n= Upgrade the build system =\n")
        else:
            print("\n= Setup the build system =\n")

        if build_virtualenv:
            delete_virtualenv()

        try:
            print("* Create the python virtualenv")
            create_virtualenv()

            if use_chroot():
                print("* Install broot package")
                install_broot()
            else:
                print("* Install osbuild package")
                install_osbuild()
        except Exception, e:
            delete_virtualenv()
            raise e

    if main_repo in changed and not is_buildbot():
        print("* Pull latest %s" % main_repo)

        os.chdir(get_base_dir())
        subprocess.check_call(["git", "pull", "--ff-only", "-q"])


def get_available_commands():
    available_commands = []
    for command in os.listdir(get_commands_dir()):
        if ".py" not in command:
            available_commands.append(command)

    return available_commands


def print_help():
    print("\n= Available commands =\n")
    print("\n".join(get_available_commands()))
    print("\nSee also %s" % docs_url)


def get_command_path(command):
    if command not in get_available_commands():
        return None

    return os.path.join(get_commands_dir(), command)


def run_command(command):
    if use_chroot():
        os.chdir(get_broot_dir())

        args = ["sudo", get_bin_path("broot"), "run"]

        prefs = get_chroot_prefs()
        if prefs is not None:
            mirror = prefs.get("mirror", None)
            if mirror is not None:
                args.extend(["--mirror", mirror])

        args.append("/home/broot/sugar-build/osbuild")
    else:
        command_path = get_command_path(command)
        if command_path is None:
            print_help()
            sys.exit(1)

        args = [get_bin_path("python"), command_path]

        os.environ["PATH"] = os.path.expandvars("$PATH:%s" % get_bin_path())

        if command == "shell":
            print_help()

    if len(sys.argv) > 2:
        args.extend(sys.argv[2:])

    return subprocess.Popen(args)


def check_lock():
    try:
        fcntl.lockf(get_lock_file(), fcntl.LOCK_EX | fcntl.LOCK_NB)
    except IOError:
        return False

    return True


def main():
    if os.geteuid() == 0:
        print("osbuild should not be run as root.")
        sys.exit(1)

    if not check_lock():
        print("Another osbuild instance is running.")
        sys.exit(1)

    if "OSBUILD_SHELL" in os.environ:
        print("Don't run osbuild inside an osbuild shell, you can just "
              "run the commands directly.")
        sys.exit(1)

    if not check_dependencies():
        sys.exit(1)

    os.environ["PIP_DOWNLOAD_CACHE"] = get_cache_dir()

    if len(sys.argv) > 1:
        command = sys.argv[1]
    else:
        command = "shell"

    pre_upgrade = is_buildbot() or command == "shell"
    post_upgrade = not pre_upgrade and not command == "clean"

    if not get_virtualenv_exists():
        setup()
    elif pre_upgrade:
        changed = check_upgrade()
        if changed is not None:
            setup(changed)

    process = run_command(command)

    if post_upgrade:
        changed = check_upgrade()
    else:
        changed = None

    result = process.wait()

    if changed is not None:
        setup(changed)

    if result != 0:
        sys.exit(result)

    if command == "clean":
        clean()

if __name__ == "__main__":
    main()
