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

# This program contains code snippets from the Python six library
# released under the following LICENSE:
#
# Copyright (c) 2010-2015 Benjamin Peterson
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

# Python 2/3 compatibility
from __future__ import print_function

import argparse
import fnmatch
import glob
import logging
import logging.handlers
import os
import platform
import shutil
import stat
import subprocess
import sys
import tempfile
import threading

# Python 2/3 compatibility
if sys.version_info[0] == 2:
  def python_2_unicode_compatible(klass):
      klass.__unicode__ = klass.__str__
      klass.__str__ = lambda self: self.__unicode__().encode('utf-8')
      return klass

  # Enforce subprocesses to use 'utf-8' in Python 2
  reload(sys)
  sys.setdefaultencoding("utf-8")

  # Aliases
  text_type = unicode
  import itertools
  filter = itertools.ifilter
  map = itertools.imap
  iteritems = dict.iteritems

  from urllib import urlretrieve
elif sys.version_info[0] == 3:
  def python_2_unicode_compatible(klass):
      return klass

  def my_str(a):
      # The following is a Hack to avoid the following error on Python 3.4 for Windows.
      #    UnicodeEncodeError: 'charmap' codec can't encode character '\u2192' in position xxx:
      #    character maps to <undefined>
      return str(a).encode('utf-8').decode(sys.stdout.encoding, errors='replace')

  # Aliases
  text_type = my_str
  iteritems = dict.items

  from urllib.request import urlretrieve
else:
  sys.exit('Unsupported Python version')

# Fixate the path separator as '\' on Windows Platform
# even if users are on CYGWIN/MSYS2 environment
if platform.system() == "Windows":
    os.path.sep = "\\"

# Reset MSYSTEM environment variable to enforce native-WINDOWS
# behavior to subprocesses.
if "MSYSTEM" in os.environ:
    del os.environ["MSYSTEM"]

if platform.system().startswith("MSYS"):
    # In MSYS platform, realpath has a strange behavior.
    #    os.path.realpath("c:\a\b\c") => \:\a\b\c
    g_linja_path   = os.path.abspath(os.path.normpath(__file__))
else:
    g_linja_path   = os.path.abspath(os.path.realpath(__file__))
g_lean_bin_dir     = os.path.dirname(g_linja_path)
g_phony_targets    = ["all", "clean", "tags", "clear-cache"]
g_project_filename = ".project"
g_lean_path        = "USE DEFAULT" # System will search automatically
g_leantags_path    = "USE DEFAULT" # System will search automatically
g_ninja_path       = "USE DEFAULT" # System will search automatically
g_flycheck_header  = "FLYCHECK_BEGIN"
g_flycheck_footer  = "FLYCHECK_END"
g_lean_bin_dep_flag= "@LEAN_BIN_DEP@" == "ON"

g_logger = logging.getLogger('linja_logger')
g_debug_mode = False

@python_2_unicode_compatible
class FlycheckItem:
    def __init__(self, filename, lineno, colno, ty, msg):
        self.filename = filename
        self.lineno   = lineno
        self.colno    = colno
        self.ty       = ty
        self.msg      = msg
        pass
    def __str__(self):
        ret = g_flycheck_header + " " + self.ty.upper() + "\n"
        ret += "%s:%d:%d: %s: %s" % (self.filename, self.lineno, self.colno, self.ty, self.msg) + "\n"
        ret += g_flycheck_footer
        return ret
    def __lt__(self, other):
        return (self.filename, self.ty, self.lineno, self.colno) < (other.filename, other.ty, other.lineno, other.colno)
    def loc(self):
        return (self.filename, self.ty, self.lineno, self.colno)
    @classmethod
    def fromString(cls, target, text):
        lines     = text.strip().splitlines()
        # Throw the first and last lines (header/footer)
        lines     = lines[1:-1]
        try:
          firstLine = lines[0]
          items     = [item.strip() for item in firstLine.split(":")]
          filename  = items[0]
          lineno    = int(items[1])
          colno     = int(items[2])
          ty        = items[3]
          msg       = ":".join(items[4:]) + "\n" + "\n".join(lines[1:])
          msg       = msg.strip()
          return cls(filename, lineno, colno, ty, msg)
        except:
          return cls(target, 1, 0, "error", " ".join(lines))

@python_2_unicode_compatible
class FlycheckItemList:
    def __init__(self, items):
        self.items = items
    def __str__(self):
        return "\n".join([text_type(item) for item in self.items])
    def __getitem__(self, i):
        return self.items[i]
    def __len__(self):
        return len(self.items)
    def sort(self):
        self.items = sorted(self.items)
    def filter(self, pred):
        self.items = list(filter(pred, self.items))
    def append(self, item):
        self.items.append(item)
    def truncate(self, n):
        del self.items[n:]
    def removeExtraItemsStartswith(self, text):
        self.sort()
        newItems = self.items[:1]
        i = 1
        while i < len(self.items):
            prev_item = self.items[i-1]
            cur_item = self.items[i]
            if not cur_item.msg.startswith(text) or prev_item.loc() != cur_item.loc():
                newItems.append(cur_item)
            i += 1
        self.items = newItems

    @classmethod
    def fromString(cls, target, text):
        items = []
        tmpBuffer = ""
        ignore = True
        for line in text.splitlines():
            # I had to add the following line to avoid a crash on Python 3.4 for Windows
            line = line.decode("utf-8")
            if line.startswith(g_flycheck_header):
                tmpBuffer = tmpBuffer + line + "\n"
                ignore = False
            elif line.startswith(g_flycheck_footer):
                tmpBuffer = tmpBuffer + line + "\n"
                items.append(FlycheckItem.fromString(target, tmpBuffer.strip()))
                tmpBuffer = ""
                ignore = True
            elif not ignore:
                tmpBuffer = tmpBuffer + line + "\n"
        return cls(items)

def init_logger():
    formatter = logging.Formatter('[%(levelname)s] %(asctime)s %(message)s')
    streamHandler = logging.StreamHandler()
    streamHandler.setFormatter(formatter)
    g_logger.addHandler(streamHandler)
    if g_debug_mode == True:
        fileHandler = logging.FileHandler('./linja.log')
        fileHandler.setFormatter(formatter)
        g_logger.addHandler(fileHandler)

def log(msg):
    print(msg, file=sys.stderr)

def log_nonewline(msg):
    print(("\r%s" % msg), end=' ', file=sys.stderr)
    sys.stderr.flush()

def error(msg):
    log("Error: %s" % msg)
    exit(1)

class LinjaException(Exception):
    """Custom Exception"""
    def __init__(self, msg):
        self.msg = msg
    def __str__(self):
        return self.msg

def give_exec_permission(filename):
    """chmod +x filename"""
    st = os.stat(filename)
    os.chmod(filename, st.st_mode | stat.S_IEXEC)

def show_download_progress(*data):
    file_size = int(data[2]/1000)
    total_packets = data[2]/data[1]
    downloaded_packets = data[0]
    log_nonewline("Download: size\t= %i kb, packet: %i/%i" % (file_size, downloaded_packets, total_packets+1))

def get_ninja_url():
    prefix = "https://leanprover.github.io/bin/"
    if platform.architecture()[0] == "64bit":
        if platform.system() == "Linux":
            return prefix + "ninja-1.5.1-linux-x86_64"
        elif platform.system() == "Windows":
            return prefix + "ninja-1.5.1-win.exe"
        elif platform.system() == "Darwin":
            return prefix + "ninja-1.5.1-osx"
    if platform.architecture()[0] == "32bit":
        if platform.system() == "Linux":
            return prefix + "ninja-1.5.1-linux-i386"
        elif platform.system() == "Windows":
            pass # TODO(soonhok): add support
        elif platform.system() == "Darwin":
            pass # TODO(soonhok): add support
    error("we do not have ninja executable for this platform: %s" % platform.platform())

def build_ninja_and_save_at(ninja_path, platform):
    saved_current_dir    = os.getcwd()
    tempdir              = tempfile.mkdtemp()
    build_dir            = os.path.join(tempdir, "ninja")
    built_ninja_path     = os.path.join(build_dir, "ninja")
    cmd_clone_ninja      = ["git", "clone", "git://github.com/martine/ninja.git"]
    cmd_checkout_release = ["git", "checkout", "release"]
    cmd_bootstrap        = [os.path.join(build_dir, "configure.py"), "--bootstrap", "--platform", platform]
    try:
        os.chdir(tempdir)
        if subprocess.call(cmd_clone_ninja):
            raise LinjaException("Failed to clone ninja repository")
        os.chdir(build_dir)
        if subprocess.call(cmd_checkout_release):
            raise LinjaException("Failed to checkout release branch of ninja")
        if subprocess.call(cmd_bootstrap):
            raise LinjaException("Failed to build ninja")
        shutil.copy2(built_ninja_path, ninja_path)
    except IOError as e:
        error(e)
    except LinjaException as e:
        error(e)
    finally:
        os.chdir(saved_current_dir)
        shutil.rmtree(tempdir)
    return ninja_path

def download_ninja_and_save_at(ninja_path):
    if platform.system().startswith("CYGWIN"):
        return build_ninja_and_save_at(ninja_path, "linux")
    else:
        url = get_ninja_url()
        log("Downloading ninja: %s ===> %s\n" % (url, ninja_path))
        tempdir = tempfile.mkdtemp()
        temp_download_path = os.path.join(tempdir, os.path.split(ninja_path)[1])
        urlretrieve(url, temp_download_path, show_download_progress)
        log("\n")
        if not os.path.isfile(temp_download_path):
            error("failed to download ninja executable from %s" % url)
        shutil.copy2(temp_download_path, ninja_path)
        give_exec_permission(ninja_path)
        return ninja_path

def find_file_upward(name, path = os.getcwd()):
    project_file = os.path.join(path, name)
    if os.path.isfile(project_file):
        return path
    up = os.path.dirname(path)
    if up != path:
        return find_file_upward(name, up)
    return None

def escape_ninja_char(name):
    return name.replace("$", "$$").replace(":", "$:").replace(" ", "$ ")

def normalize_drive_name(name):
    if platform.system() == "Windows":
        drive, path = os.path.splitdrive(name)
        if drive == None:
            return name
        else:
            # Leo: return drive.lower() + path
            return path
    else:
        return name

def process_target(target):
    if target in g_phony_targets:
        return target
    return normalize_drive_name(os.path.abspath(target))

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('"')
            if "sbin" in path:
                continue
            exe_file = os.path.join(path, program)
            if is_exe(exe_file):
                return exe_file
    return None

def find_location(exec_name, findLocal=True):
    if findLocal:
        pathname = os.path.join(os.path.dirname(g_linja_path), exec_name)
        if os.path.isfile(pathname):
            return pathname
    pathname = which(exec_name) or os.path.join(g_lean_bin_dir, exec_name)
    pathname = os.path.abspath(pathname)
    return pathname

def check_requirements():
    global g_lean_path, g_leantags_path, g_ninja_path
    leantags_exec_name = "leantags"
    lean_exec_name  = "lean"
    ninja_exec_name = "ninja"
    if platform.system() == "Windows" or platform.system().startswith("MSYS"):
        lean_exec_name  = "lean.exe"
        ninja_exec_name = "ninja.exe"

    if g_lean_path == "USE DEFAULT":
        g_lean_path = find_location(lean_exec_name, True)
    if g_leantags_path == "USE DEFAULT":
        g_leantags_path = find_location(leantags_exec_name, True)
    if g_ninja_path == "USE DEFAULT":
        g_ninja_path = find_location(ninja_exec_name, False)
    if not os.path.isfile(g_lean_path):
        error("cannot find lean executable at " + os.path.abspath(g_lean_path))
    if not os.path.isfile(g_leantags_path):
        error("cannot find leantags executable at " + os.path.abspath(g_leantags_path))
    if not os.path.isfile(g_ninja_path):
        g_ninja_path = download_ninja_and_save_at(g_ninja_path)

def process_args(args):
    if (args.flycheck == False and args.flycheck_max_messages != None):
        error("Please use --flycheck option with --flycheck-max-messages option.")
    args.targets = list(map(process_target, args.targets))
    if args.directory:
        os.chdir(args.directory)
    args.project_dir = find_file_upward(g_project_filename)
    if args.project_dir:
        os.chdir(args.project_dir)
    if args.cache:
        args.cache = process_target(args.cache)
        if len(args.targets) != 1:
            error("--cache option can only be used with one target")
        if not args.cache.endswith(".lean") and not args.cache.endswith(".hlean"):
            error("cache argument has to end with .lean or .hlean")
        if args.cache.endswith(".lean"):
            args.cache = args.cache[:-4] + "clean"
        else:
            args.cache = args.cache[:-5] + "clean"
    args.phony_targets = list(set(g_phony_targets) & set(args.targets))
    if args.verbose:
        g_logger.setLevel(logging.INFO)
    return args

def get_lean_options(args):
    args.lean_options = []
    if args.flycheck:
        args.lean_options.append("--flycheck")
    if args.discard:
        args.lean_options.append("--discard")
    if args.memory:
        args.lean_options += ["-M", args.memory]
    if args.trust:
        args.lean_options += ["-t", args.trust]
    if args.to_axiom:
        args.lean_options.append("--to_axiom")
    if args.cache:
        args.lean_options += ["-c", args.cache]
    if args.lean_config_option:
        for item in args.lean_config_option:
            args.lean_options.append("-D" + item)
    return args

def parse_arg(argv):
    parser = argparse.ArgumentParser(description='linja: ninja build wrapper for Lean theorem prover.')
    parser.add_argument('--flycheck', '-F', action='store_true', default=False, help="Use --flycheck option for Lean.")
    parser.add_argument('--flycheck-max-messages', action='store', type=int, default=None, const=999999, nargs='?', help="Number of maximum flycheck messages to display.")
    parser.add_argument('--cache', action='store', help="Use specified cache (clean) file.")
    parser.add_argument('--directory', '-C', action='store', help="change to DIR before doing anything else.")
    parser.add_argument('--lean-config-option', '-D', action='append', help="set a Lean configuration option (name=value)")
    parser.add_argument('--verbose', '-v', action='store_true', help="turn on verbose option")
    parser.add_argument('--memory', '-M', action='store', default=None, const=1, nargs='?', help="maximum amount of memory that can be used by Lean [default=unbounded]")
    parser.add_argument('--keep-going', '-k', action='store', default=None, const=1, nargs='?', help="keep going until N jobs fail [default=1]")
    parser.add_argument('--discard', '-r', action='store_true', default=False, help="discard the proof of imported theorems after checking")
    parser.add_argument('--to_axiom', '-X', action='store_true', default=False, help="discard proofs of all theorems after checking them, i.e., theorems become axioms after checking")
    parser.add_argument('--trust', '-t', action='store_true', default=False, help="trust level [default: max]")
    parser.add_argument('targets', nargs='*')
    args = parser.parse_args(argv)
    check_requirements()
    args = process_args(args)
    args = get_lean_options(args)
    return args

def debug_status(args):
    print("Working Directory =", os.getcwd())
    print("")
    for key, val in iteritems(vars(args)):
        print("Option[" + key + "] =", val)
    print("")
    print("linja path    =", g_linja_path)
    print("lean/bin dir  =", g_lean_bin_dir)
    print("phony targets =", g_phony_targets)
    print("lean path     =", g_lean_path)
    print("leantags path =", g_leantags_path)
    print("ninja path    =", g_ninja_path)

def handle_flycheck_failure(out, err, args):
    if len(args.targets) == 0:
        error("handle_flycheck_failure is called without targets")
    target = args.targets[0]
    failed = set()
    for line in out.splitlines():
        if line.startswith("FAILED:"):
            for lean_file in find_lean_files(args):
                lean = lean_file['lean']
                if lean in line and lean != target:
                    failed.add(lean)
    for failed_file in failed:
        print(g_flycheck_header, "ERROR")
        print("%s:1:0: error: failed to compile %s" % (target, failed_file))
        print(g_flycheck_footer)
    if err:
        print(g_flycheck_header, "ERROR")
        print("%s:1:0: error: %s" % (target, err.strip()))
        print(g_flycheck_footer)
    if failed:
        call_lean(target, args)

def process_lean_output(target, out, args, using_hlean):
    n = args.flycheck_max_messages
    if target.endswith(".olean"):
        if using_hlean:
            target = target[:-5] + "hlean"
        else:
            target = target[:-5] + "lean"
    if (not target.endswith(".lean") and not using_hlean) or (not target.endswith(".hlean") and using_hlean):
        print(out)
        return
    # Parse, filter, and remove extra items
    flycheckItemList = FlycheckItemList.fromString(target, out)
    flycheckItemList.filter(lambda item: item.filename == target)
    flycheckItemList.removeExtraItemsStartswith("failed to add declaration")
    # Only keep n items in the list.
    # Add tooManyItemsError at the end if we truncated the list
    if n and len(flycheckItemList) > n:
        count = len(flycheckItemList)
        flycheckItemList.truncate(n)
        tooManyItemsError = FlycheckItem(target, 1, 0, "error", "For performance, we only display %d errors/warnings out of %d. (lean-flycheck-max-messages-to-display)" % (n, count))
        flycheckItemList.append(tooManyItemsError)
    print(flycheckItemList)

def call_ninja(args):
    targets = []
    for item in args.targets:
        if item.endswith(".lean"):
            targets.append(item[:-4] + "olean")
        elif item.endswith(".hlean"):
            targets.append(item[:-5] + "olean")
        else:
            targets.append(item)
    proc_out = proc_err = None
    if args.flycheck:
        proc_out = subprocess.PIPE
        proc_err = subprocess.PIPE
    ninja_option = []
    if args.keep_going:
        ninja_option += ["-k", args.keep_going]
    proc = subprocess.Popen([g_ninja_path] + ninja_option + targets, stdout=proc_out, stderr=proc_err)
    (out, err) = proc.communicate()
    if out:
      out = out.decode('utf-8')
    if err:
      err = err.decode('utf-8')
    if args.flycheck:
        if len(args.targets) == 1 and (args.targets[0].endswith(".lean") or args.targets[0].endswith(".hlean")):
            process_lean_output(targets[0], out, args, args.targets[0].endswith(".hlean"))
            handle_flycheck_failure(out, err, args)
        else:
            print(out + err)

    return proc.returncode

def call_lean(filename, args):
    proc = subprocess.Popen([g_lean_path] + args.lean_options + [filename],
                            stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
    out = proc.communicate()[0].decode('utf-8')
    process_lean_output(filename, out, args, filename.endswith(".hlean"))
    return proc.returncode

def get_lean_names(lean_file, args, using_hlean):
    lean_file = os.path.abspath(lean_file)
    basename, ext = os.path.splitext(lean_file)
    basename = normalize_drive_name(basename)
    item = {"base" : basename}
    lean_name_exts = ["lean", "olean", "clean", "ilean", "d"]
    for ext in lean_name_exts:
        if ext == "lean" and using_hlean:
            item[ext] = basename + ".hlean"
        else:
            item[ext] = basename + "." + ext
    if args.cache and len(args.targets) == 1 and item['lean'] == args.targets[0]:
        item['clean'] = args.cache
    return item

def find_files(directory, pattern):
    if "/" in pattern:
        return glob.glob(os.path.join(directory, pattern))
    matches = []
    for root, dirnames, filenames in os.walk(directory):
        for filename in fnmatch.filter(filenames, pattern):
            matches.append(normalize_drive_name(os.path.join(root, filename)))
    return matches

def find_lean_files(args):
    """Find lean files under project directory. Include and exclude
    files based on patterns in .project file. Use static cache to
    compute only once and reuse it"""
    project_dir = args.project_dir
    if find_lean_files.cached_list != []:
        return find_lean_files.cached_list
    files = set()
    include_patterns, exclude_patterns = [], []
    with open(os.path.join(project_dir, g_project_filename), 'r') as f:
        for line in f:
            c = line[0]
            if c == '+':
                include_patterns.append(line[1:].strip())
            elif c == '-':
                exclude_patterns.append(line[1:].strip())
            elif c == '#':
                pass # Comment
            elif c == 'T':
                pass # TAG
            elif c == 'O':
                pass # Lean Option
    for pattern in include_patterns:
        files |= set(find_files(project_dir, pattern))
    for pattern in exclude_patterns:
        files -= set(find_files(project_dir, pattern))
    has_lean  = False
    has_hlean = False
    for file in files:
        if file.endswith(".lean"):
            has_lean = True
        if file.endswith(".hlean"):
            has_hlean = True
    if has_lean and has_hlean:
        error("project cannot mix .lean and .hlean files")
    for file in args.targets:
        if file.endswith(".lean") or file.endswith(".hlean"):
            files.add(file)
        elif file.endswith(".olean"):
            if has_hlean:
                file.add(file[:-5] + "hlean")
            else:
                file.add(file[:-5] + "lean")
    for f in files:
        find_lean_files.cached_list.append(get_lean_names(f, args, has_hlean))
    return find_lean_files.cached_list

# Initialize static variable
find_lean_files.cached_list = []

def clear_cache(args):
    files = find_lean_files(args)
    files = find_lean_files(args)
    files = find_lean_files(args)
    files = find_lean_files(args)
    num_of_files = len(files)
    i = 0
    for item in files:
        i += 1
        clean_file = item['clean']
        if os.path.isfile(clean_file):
            sys.stderr.write("[%i/%i] clear cache... % -80s\r" % (i, num_of_files, clean_file))
            sys.stderr.flush()
            os.remove(clean_file)
    if num_of_files > 0:
        sys.stderr.write("\n")

def build_olean(lean, olean, clean, dlean, ilean, base):
    (lean, olean, clean, dlean, ilean, base) = list(map(escape_ninja_char, (lean, olean, clean, dlean, ilean, base)))
    if clean.startswith(base):
        str = """build %s %s %s: LEAN %s | %s""" % (olean, ilean, clean, lean, dlean)
    else:
        str = """build %s %s: LEAN %s | %s""" % (olean, ilean, lean, dlean)
    if g_lean_bin_dep_flag:
        str += " %s" % escape_ninja_char(normalize_drive_name(g_lean_path))
    str += "\n"
    str += "  DLEAN_FILE=%s\n" % dlean
    str += "  OLEAN_FILE=%s\n" % olean
    str += "  CLEAN_FILE=%s\n" % clean
    str += "  ILEAN_FILE=%s\n" % ilean
    return str

def make_build_ninja(args):
    with open(os.path.join(args.project_dir, "build.ninja"), "w") as f:
        lean_files = find_lean_files(args)

        print("""rule CLEAN""", file=f)
        print("""  command = """, end=' ', file=f)
        print(""""%s" -t clean""" % g_ninja_path, file=f)
        print("""  description = Cleaning all built files...""", file=f)

        print("""rule LEAN""", file=f)
        print("""  depfile = ${DLEAN_FILE}""", file=f)
        print("""  command = "%s" %s $in -o "${OLEAN_FILE}" -c "${CLEAN_FILE}" -i "${ILEAN_FILE}" """ \
            % (g_lean_path, " ".join(args.lean_options)), file=f)

        print("""rule LEANTAGS""", file=f)
        print("""  command = """, end=' ', file=f)
        if platform.system() == "Windows":
            print("python ", end=' ', file=f)
        print(""""%s" --relative -- $in """ % (g_leantags_path), file=f)

        print("build all: phony", end=' ', file=f)
        for item in lean_files:
            print(" " + escape_ninja_char(item['olean']), end=' ', file=f)
        print("", file=f)

        tags_file = "TAGS"
        print("build tags: phony " + tags_file, file=f)
        print("build " + tags_file + ": LEANTAGS", end=' ', file=f)
        for item in lean_files:
            print(" " + escape_ninja_char(item['ilean']), end=' ', file=f)
        print("", file=f)

        print("""build clean: CLEAN""", file=f)

        for item in lean_files:
            print(build_olean(item['lean'], item['olean'], item['clean'], item['d'], item['ilean'], item['base']), file=f)

        print("""default all""", file=f)

def escape_dep(s):
    return s.replace(" ", "\\ ")

def make_deps(lean_file, dlean_file, olean_file):
    with open(dlean_file, "w") as f:
        deps = []
        proc = subprocess.Popen([g_lean_path, "--deps", lean_file], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
        output = proc.communicate()[0].decode('utf-8')
        print(escape_dep(olean_file) + ": \\", file=f)
        for olean_file in output.strip().splitlines():
            if olean_file:
                deps.append(normalize_drive_name(os.path.abspath(olean_file)))
        deps = list(map(escape_dep, deps))
        deps_str = " " + (" \\\n ".join(deps))
        print(deps_str, file=f)

def make_deps_all_files(args):
    lean_files = find_lean_files(args)
    threads = []
    i, num_of_files = 1, len(lean_files)
    for item in lean_files:
        lean_file   = item['lean']
        dlean_file  = item['d']
        olean_file  = item['olean']
        if not os.path.isfile(dlean_file) or os.path.getmtime(dlean_file) < os.path.getmtime(lean_file):
            thread = threading.Thread(target=make_deps, args = [lean_file, dlean_file, olean_file])
            sys.stderr.write("[%i/%i] generating dep... % -80s" % (i, num_of_files, dlean_file))
            if args.flycheck == True:
                sys.stderr.write("\n")
            else:
                sys.stderr.write("\r")
            sys.stderr.flush()
            thread.start()
            threads.append(thread)
        i += 1
    for thread in threads:
        thread.join()
    if threads != []:
        sys.stderr.write("\n")

def main(argv=None):
    init_logger()
    if argv is None:
        argv = sys.argv[1:]
    args = parse_arg(argv)
    if args.project_dir:
        if args.targets == ["clear-cache"]:
            args.targets = []
            clear_cache(args)
            return 0
        if not "clean" in args.targets:
            make_deps_all_files(args)
        make_build_ninja(args)
        return call_ninja(args)

    else: # NO Project Directory Found
        if args.phony_targets:
            error("cannot find project directory. Make sure that you have " \
                   + g_project_filename + " file at the project root.")
        returncode = 0
        for filename in args.targets:
            if os.path.isfile(filename) and (filename.endswith(".lean") or filename.endswith(".hlean")):
                returncode |= call_lean(filename, args)
        return returncode

    return 0

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