#!/usr/bin/env python

import errno
import hashlib
import fnmatch
import os
import platform
import re
import repo
import subprocess
import sys

from lldbbuild import *

#### SETTINGS ####


def LLVM_HASH_INCLUDES_DIFFS():
    return False

# For use with Xcode-style builds

def process_vcs(vcs):
    return {
        "svn": VCS.svn,
        "git": VCS.git
    }[vcs]

def process_root(name):
    return {
        "llvm": llvm_source_path(),
        "clang": clang_source_path(),
        "ninja": ninja_source_path()
    }[name]

def process_repo(r):
    return {
        'name': r["name"],
        'vcs': process_vcs(r["vcs"]),
        'root': process_root(r["name"]),
        'url': r["url"],
        'ref': r["ref"]
    }

def XCODE_REPOSITORIES():
    override = repo.get_override()
    if override:
        return [process_repo(r) for r in override]
    identifier = repo.identifier()
    if identifier == None:
        identifier = "<invalid>" # repo.find will just use the fallback file
    set = repo.find(identifier)
    return [process_repo(r) for r in set]


def get_c_compiler():
    return subprocess.check_output([
        'xcrun',
        '--sdk', 'macosx',
        '-find', 'clang'
    ]).rstrip()


def get_cxx_compiler():
    return subprocess.check_output([
        'xcrun',
        '--sdk', 'macosx',
        '-find', 'clang++'
    ]).rstrip()

#                 CFLAGS="-isysroot $(xcrun --sdk macosx --show-sdk-path) -mmacosx-version-min=${DARWIN_DEPLOYMENT_VERSION_OSX}" \
#                        LDFLAGS="-mmacosx-version-min=${DARWIN_DEPLOYMENT_VERSION_OSX}" \


def get_deployment_target():
    return os.environ.get('MACOSX_DEPLOYMENT_TARGET', None)


def get_c_flags():
    cflags = ''
    # sdk_path = subprocess.check_output([
    #     'xcrun',
    #     '--sdk', 'macosx',
    #     '--show-sdk-path']).rstrip()
    # cflags += '-isysroot {}'.format(sdk_path)

    deployment_target = get_deployment_target()
    if deployment_target:
        # cflags += ' -mmacosx-version-min={}'.format(deployment_target)
        pass

    return cflags


def get_cxx_flags():
    return get_c_flags()


def get_common_linker_flags():
    linker_flags = ""
    deployment_target = get_deployment_target()
    if deployment_target:
        # if len(linker_flags) > 0:
        #     linker_flags += ' '
        # linker_flags += '-mmacosx-version-min={}'.format(deployment_target)
        pass

    return linker_flags


def get_exe_linker_flags():
    return get_common_linker_flags()


def get_shared_linker_flags():
    return get_common_linker_flags()


def CMAKE_FLAGS():
    return {
        "Debug": [
            "-DCMAKE_BUILD_TYPE=RelWithDebInfo",
            "-DLLVM_ENABLE_ASSERTIONS=ON",
        ],
        "DebugClang": [
            "-DCMAKE_BUILD_TYPE=Debug",
            "-DLLVM_ENABLE_ASSERTIONS=ON",
        ],
        "Release": [
            "-DCMAKE_BUILD_TYPE=Release",
            "-DLLVM_ENABLE_ASSERTIONS=ON",
        ],
        "BuildAndIntegration": [
            "-DCMAKE_BUILD_TYPE=Release",
            "-DLLVM_ENABLE_ASSERTIONS=OFF",
        ],
    }


def CMAKE_ENVIRONMENT():
    return {
    }

#### COLLECTING ALL ARCHIVES ####


def collect_archives_in_path(path):
    files = os.listdir(path)
    # Only use libclang and libLLVM archives, and exclude libclang_rt
    regexp = "^lib(clang[^_]|LLVM|gtest).*$"
    return [
        os.path.join(
            path,
            file) for file in files if file.endswith(".a") and re.match(
            regexp,
            file)]


def archive_list():
    paths = library_paths()
    archive_lists = [collect_archives_in_path(path) for path in paths]
    return [archive for archive_list in archive_lists for archive in archive_list]


def write_archives_txt():
    f = open(archives_txt(), 'w')
    for archive in archive_list():
        f.write(archive + "\n")
    f.close()

#### COLLECTING REPOSITORY MD5S ####


def source_control_status(spec):
    vcs_for_spec = vcs(spec)
    if LLVM_HASH_INCLUDES_DIFFS():
        return vcs_for_spec.status() + vcs_for_spec.diff()
    else:
        return vcs_for_spec.status()


def source_control_status_for_specs(specs):
    statuses = [source_control_status(spec) for spec in specs]
    return "".join(statuses)


def all_source_control_status():
    return source_control_status_for_specs(XCODE_REPOSITORIES())


def md5(string):
    m = hashlib.md5()
    m.update(string)
    return m.hexdigest()


def all_source_control_status_md5():
    return md5(all_source_control_status())

#### CHECKING OUT AND BUILDING LLVM ####


def apply_patches(spec):
    files = os.listdir(os.path.join(lldb_source_path(), 'scripts'))
    patches = [
        f for f in files if fnmatch.fnmatch(
            f, spec['name'] + '.*.diff')]
    for p in patches:
        run_in_directory(["patch",
                          "-p0",
                          "-i",
                          os.path.join(lldb_source_path(),
                                       'scripts',
                                       p)],
                         spec['root'])


def check_out_if_needed(spec):
    if not os.path.isdir(spec['root']):
        vcs(spec).check_out()
        apply_patches(spec)


def all_check_out_if_needed():
    map(check_out_if_needed, XCODE_REPOSITORIES())


def should_build_llvm():
    if build_type() == BuildType.Xcode:
        # TODO use md5 sums
        return True


def do_symlink(source_path, link_path):
    print "Symlinking " + source_path + " to " + link_path
    if not os.path.exists(link_path):
        os.symlink(source_path, link_path)


def setup_source_symlink(repo):
    source_path = repo["root"]
    link_path = os.path.join(lldb_source_path(), os.path.basename(source_path))
    do_symlink(source_path, link_path)


def setup_source_symlinks():
    map(setup_source_symlink, XCODE_REPOSITORIES())


def setup_build_symlink():
    # We don't use the build symlinks in llvm.org Xcode-based builds.
    if build_type() != BuildType.Xcode:
        source_path = package_build_path()
        link_path = expected_package_build_path()
        do_symlink(source_path, link_path)


def should_run_cmake(cmake_build_dir):
    # We need to run cmake if our llvm build directory doesn't yet exist.
    if not os.path.exists(cmake_build_dir):
        return True

    # Wee also need to run cmake if for some reason we don't have a ninja
    # build file.  (Perhaps the cmake invocation failed, which this current
    # build may have fixed).
    ninja_path = os.path.join(cmake_build_dir, "build.ninja")
    return not os.path.exists(ninja_path)


def cmake_environment():
    cmake_env = join_dicts(os.environ, CMAKE_ENVIRONMENT())
    return cmake_env


def is_executable(path):
    return os.path.isfile(path) and os.access(path, os.X_OK)


def find_executable_in_paths(program, paths_to_check):
    program_dir, program_name = os.path.split(program)
    if program_dir:
        if is_executable(program):
            return program
    else:
        for path_dir in paths_to_check:
            path_dir = path_dir.strip('"')
            executable_file = os.path.join(path_dir, program)
            if is_executable(executable_file):
                return executable_file
    return None


def find_cmake():
    # First check the system PATH env var for cmake
    cmake_binary = find_executable_in_paths(
        "cmake", os.environ["PATH"].split(os.pathsep))
    if cmake_binary:
        # We found it there, use it.
        return cmake_binary

    # Check a few more common spots.  Xcode launched from Finder
    # will have the default environment, and may not have
    # all the normal places present.
    extra_cmake_dirs = [
        "/usr/local/bin",
        "/opt/local/bin",
        os.path.join(os.path.expanduser("~"), "bin")
    ]

    if platform.system() == "Darwin":
        # Add locations where an official CMake.app package may be installed.
        extra_cmake_dirs.extend([
            os.path.join(
                os.path.expanduser("~"),
                "Applications",
                "CMake.app",
                "Contents",
                "bin"),
            os.path.join(
                os.sep,
                "Applications",
                "CMake.app",
                "Contents",
                "bin")])

    cmake_binary = find_executable_in_paths("cmake", extra_cmake_dirs)
    if cmake_binary:
        # We found it in one of the usual places.  Use that.
        return cmake_binary

    # We couldn't find cmake.  Tell the user what to do.
    raise Exception(
        "could not find cmake in PATH ({}) or in any of these locations ({}), "
        "please install cmake or add a link to it in one of those locations".format(
            os.environ["PATH"], extra_cmake_dirs))


def cmake_flags():
    cmake_flags = CMAKE_FLAGS()[lldb_configuration()]
    cmake_flags += ["-GNinja",
                    "-DCMAKE_C_COMPILER={}".format(get_c_compiler()),
                    "-DCMAKE_CXX_COMPILER={}".format(get_cxx_compiler()),
                    "-DCMAKE_INSTALL_PREFIX={}".format(expected_package_build_path_for("llvm")),
                    "-DCMAKE_C_FLAGS={}".format(get_c_flags()),
                    "-DCMAKE_CXX_FLAGS={}".format(get_cxx_flags()),
                    "-DCMAKE_EXE_LINKER_FLAGS={}".format(get_exe_linker_flags()),
                    "-DCMAKE_SHARED_LINKER_FLAGS={}".format(get_shared_linker_flags()),
                    "-DHAVE_CRASHREPORTER_INFO=1"]
    deployment_target = get_deployment_target()
    if deployment_target:
        cmake_flags.append(
            "-DCMAKE_OSX_DEPLOYMENT_TARGET={}".format(deployment_target))
    return cmake_flags


def run_cmake(cmake_build_dir, ninja_binary_path):
    cmake_binary = find_cmake()
    print "found cmake binary: using \"{}\"".format(cmake_binary)

    command_line = [cmake_binary] + cmake_flags() + [
        "-DCMAKE_MAKE_PROGRAM={}".format(ninja_binary_path),
        llvm_source_path()]
    print "running cmake like so: ({}) in dir ({})".format(command_line, cmake_build_dir)

    subprocess.check_call(
        command_line,
        cwd=cmake_build_dir,
        env=cmake_environment())


def create_directories_as_needed(path):
    try:
        os.makedirs(path)
    except OSError as error:
        # An error indicating that the directory exists already is fine.
        # Anything else should be passed along.
        if error.errno != errno.EEXIST:
            raise error


def run_cmake_if_needed(ninja_binary_path):
    cmake_build_dir = package_build_path()
    if should_run_cmake(cmake_build_dir):
        # Create the build directory as needed
        create_directories_as_needed(cmake_build_dir)
        run_cmake(cmake_build_dir, ninja_binary_path)


def build_ninja_if_needed():
    # First check if ninja is in our path.  If so, there's nothing to do.
    ninja_binary_path = find_executable_in_paths(
        "ninja", os.environ["PATH"].split(os.pathsep))
    if ninja_binary_path:
        # It's on the path.  cmake will find it.  We're good.
        print "found ninja here: \"{}\"".format(ninja_binary_path)
        return ninja_binary_path

    # Figure out if we need to build it.
    ninja_build_dir = ninja_source_path()
    ninja_binary_path = os.path.join(ninja_build_dir, "ninja")
    if not is_executable(ninja_binary_path):
        # Build ninja
        command_line = ["python", "configure.py", "--bootstrap"]
        print "building ninja like so: ({}) in dir ({})".format(command_line, ninja_build_dir)
        subprocess.check_call(
            command_line,
            cwd=ninja_build_dir,
            env=os.environ)

    return ninja_binary_path


def join_dicts(dict1, dict2):
    d = dict1.copy()
    d.update(dict2)
    return d


def build_llvm(ninja_binary_path):
    cmake_build_dir = package_build_path()
    subprocess.check_call(
        [ninja_binary_path],
        cwd=cmake_build_dir,
        env=cmake_environment())


def build_llvm_if_needed():
    if should_build_llvm():
        ninja_binary_path = build_ninja_if_needed()
        run_cmake_if_needed(ninja_binary_path)
        build_llvm(ninja_binary_path)
        setup_build_symlink()

#### MAIN LOGIC ####

all_check_out_if_needed()
build_llvm_if_needed()
write_archives_txt()

sys.exit(0)
