#!/usr/bin/env python2
#
# Copyright (c) 2014 Microsoft Corporation. All rights reserved.
# Released under Apache 2.0 license as described in the file LICENSE.
#
# Author: Soonho Kong

import argparse
import fnmatch
import os
import os.path
import string
import subprocess
import sys

def lean_exe_name():
    """ Return a lean executable name """
    import platform
    if platform.system() == 'Windows':
        return "lean.exe"
    else:
        return "lean"

def find_makefile(path, makefile_names):
    """ Find makefile in a given directory.

    Args:
        path: a string of path to look up
        makefile_names: a list of strings to search

    Return:
        When found, return the full path of a makefile
        Otherwise, return False.
    """
    for makefile in makefile_names:
        makefile_pathname = os.path.join(path, makefile)
        if os.path.isfile(makefile_pathname):
            return makefile_pathname
    return False

def find_makefile_upward(path, makefile_names):
    """ Strating from a given directory, search upward to find
    a makefile

    Args:
        path: a string of path to start the search

    Return:
        When found, return the full path of a makefile
        Otherwise, return False.
    """
    makefile = find_makefile(path, makefile_names)
    if makefile:
        return makefile
    up = os.path.dirname(path)
    if up != path:
        return find_makefile_upward(up, makefile_names)
    return False

def which(program):
    """ Lookup program in a path """
    import os
    def is_exe(fpath):
        return os.path.isfile(fpath) and os.access(fpath, os.X_OK)
    fpath, fname = os.path.split(program)
    if fpath:
        if is_exe(program):
            return program
    else:
        for path in os.environ["PATH"].split(os.pathsep):
            path = path.strip('"')
            exe_file = os.path.join(path, program)
            if is_exe(exe_file):
                return exe_file
    return None

def find_lean_exe():
    """ Find a fullpath of Lean executable """
    # First Look up environment variable
    lean=os.environ.get('LEAN')
    if lean:
        return lean
    # Otherwise, Look up in PATH
    import platform
    lean = which(lean_exe_name())
    # Otherwise, look up the directory where lmake is at
    script_dir = os.path.dirname(os.path.realpath(__file__))
    lean = os.path.join(script_dir, lean_exe_name())
    if os.path.isfile(lean):
        return lean
    return False

def find_lean_opt():
    """ Return a list of lean options from env_var LEAN_OPTIONS """
    lean_opt = os.environ.get('LEAN_OPTIONS')
    return string.split(lean_opt) if lean_opt is not None else []

def call_lean(leanfile, options):
    """ Call lean with options """
    from collections import OrderedDict
    oleanfile = leanfile[:-5] + ".olean"
    lean_exe = find_lean_exe()
    lean_opt = list(OrderedDict.fromkeys(find_lean_opt() + options))
    cmd = [lean_exe] + lean_opt + [leanfile] + ["-o", oleanfile]
    subprocess.call(cmd, stderr=subprocess.STDOUT)

def get_lean(s):
    """ Given a string s, return corresponding .lean file if exists """
    # xyz.olean => realpath(xyz.lean)
    if len(s) > 6 and s[-6:] == ".olean":
        leanfile = os.path.realpath(s[:-6] + ".lean")
        return leanfile if os.path.isfile(leanfile) else None
    # xyz.lean  => realpath(xyz.lean)
    if os.path.isfile(s) and len(s) > 5 and s[-5:] == ".lean":
        return os.path.realpath(s)
    # xyz       => realpath(xyz.lean)
    if os.path.isfile(s + ".lean"):
        return os.path.realpath(s + ".lean")
    return None

def get_olean(s):
    """ Given a string s, return corresponding .olean file if exists """
    # xyz.olean => realpath(xyz.olean)
    if len(s) > 6 and s[-6:] == ".olean":
        leanfile = s[:-6] + ".lean"
        return os.path.realpath(s) if os.path.isfile(leanfile) else None
    # xyz.lean  => realpath(xyz.olean)
    if os.path.isfile(s) and len(s) > 5 and s[-5:] == ".lean":
        leanfile = os.path.realpath(s)
        return leanfile[:-5] + ".olean"
    # xyz       => realpath(xyz.olean)
    if os.path.isfile(s + ".lean"):
        return os.path.realpath(s + ".olean")
    return None

def get_target(s):
    """ Extract a target from an argument
    if we have "xyz.lean", return "xyz.olean"
    Otherwise, return s as it is. (it might be phony target such as 'clean')
    """
    oleanfile = get_olean(s)
    return oleanfile if oleanfile is not None else s

def need_to_make_always(directory, lean_options):
    """Return true if "-P" option was used previously, but not this time."""
    lean_new_options = " ".join(lean_options)
    lean_old_options = ""
    lean_options_file_name = os.path.join(directory, ".lean_options")
    need_to_write = True
    if os.path.isfile(lean_options_file_name):
        with open(lean_options_file_name, 'r') as f:
            lean_old_options = f.readline()
            need_to_write = not (lean_old_options == lean_new_options)
    if need_to_write:
        with open(lean_options_file_name, 'w') as f:
            f.write(lean_new_options)
    if ("--permissive" in lean_options) or ("-P" in lean_options):
        return False
    if ("--permissive" in lean_old_options) or ("-P" in lean_old_options):
        return True
    return False

def call_makefile(directory, makefile, makefile_options, args, lean_options):
    """ Call makefile with a target generated from a given arg """
    need_to_update = need_to_make_always(directory, lean_options)
    if need_to_update:
        makefile_options.append("--always-make")
    env_copy = os.environ.copy()
    env_copy['LEAN'] = find_lean_exe()
    lean_options_str = ' '.join(lean_options)
    if env_copy.get("LEAN_OPTIONS"):
        env_copy['LEAN_OPTIONS'] = env_copy['LEAN_OPTIONS'] + lean_options_str
    else:
        env_copy['LEAN_OPTIONS'] = lean_options_str
    cmd = ["make"]
    if makefile:
        cmd += ["--makefile", makefile]
    if directory:
        cmd += ["-C", directory]
    cmd += makefile_options
    for arg in args:
        target = get_target(arg)
        cmd.append(target)
    subprocess.call(cmd, stderr=subprocess.STDOUT, env=env_copy)

def check_positive(value):
    ivalue = int(value)
    if ivalue <= 0:
         raise argparse.ArgumentTypeError("%s is an invalid positive int value" % value)
    return ivalue

def parse_arg(argv):
    """ Parse arguments """
    parser = argparse.ArgumentParser(description='Process arguments.')
    parser.add_argument('--flycheck',   '-F', action='store_true', default=False, help="Use --flycheck option for Lean.")
    parser.add_argument('--permissive', '-P', action='store_true', default=False, help="Use --permissive option for Lean.")
    parser.add_argument('--keep-going', '-k', action='store_true', default=False, help="Continue as much as possible after an error.")
    parser.add_argument('--directory',  '-C', action='store', help="Change to directory dir before reading the makefiles or doing anything else.")
    parser.add_argument('--always-make','-B', action='store_true', default=False, help="Unconditionally make all targets.")
    parser.add_argument('--makefile',   '-f', '--file', action='store', help="Use file as a makefile.")
    parser.add_argument('--jobs',       '-j', nargs='?', default=-1, const=-2, type=check_positive, help="Specifies the number of jobs (commands) to run simultaneously.")
    parser.add_argument('--just-print', '--dry-run', '--recon', '-n', action='store_true', default=False, help="Don't actually run any commands; just print them.")
    parser.add_argument('targets', nargs='*')
    args = parser.parse_args(argv)
    lean_options = extract_lean_options(args)
    makefile_options = extract_makefile_options(args)
    return (args.directory, args.makefile, makefile_options, lean_options, args.targets)

def extract_lean_options(args):
    lean_options = []
    if args.flycheck:
        lean_options.append("--flycheck")
    if args.permissive:
        lean_options.append("--permissive")
    return lean_options

def extract_makefile_options(args):
    makefile_options = []
    if args.keep_going:
        makefile_options.append("--keep-going")
    if args.jobs == -1:
        # -j is not used
        pass
    elif args.jobs == -2:
        # -j is used but without providing number of processes
        makefile_options.append("-j")
    elif args.jobs:
        makefile_options += ["-j", args.jobs]
    if args.just_print:
        makefile_options.append("--just-print")
    if args.always_make:
        makefile_options.append("--always-make")
    return makefile_options

def main(argv=None):
    if argv is None:
        argv = sys.argv[1:]
    (directory, makefile, makefile_options, lean_options, args) = parse_arg(argv)
    working_dir = os.getcwd()
    if makefile is None and directory is None:
        makefile_names = ["GNUmakefile", "makefile", "Makefile"]
        makefile = find_makefile_upward(working_dir, makefile_names)
        if makefile:
            directory = os.path.dirname(makefile)
    if directory or makefile:
        call_makefile(directory, makefile, makefile_options, args, lean_options)
    else:
        for arg in args:
            leanfile = get_lean(arg)
            if leanfile and os.path.isfile(leanfile):
                call_lean(leanfile, lean_options)

if __name__ == "__main__":
    sys.exit(main())
