# Copyright 2017 Steven Watanabe
#
# Distributed under the Boost Software License, Version 1.0.
# (See accompanying file LICENSE_1_0.txt or copy at
# http://www.boost.org/LICENSE_1_0.txt)

from __future__ import print_function

import sys
import os
import re
import fnmatch

# Represents a sequence of arguments that must appear
# in a fixed order.
class ordered:
    def __init__(self, *args):
        self.args = args
    def match(self, command_line, pos, outputs):
        for p in self.args:
            res = try_match(command_line, pos, p, outputs)
            if res is None:
                return
            pos = res
        return pos

# Represents a sequence of arguments that can appear
# in any order.
class unordered:
    def __init__(self, *args):
        self.args = list(args)
    def match(self, command_line, pos, outputs):
        unmatched = self.args[:]
        while len(unmatched) > 0:
            res = try_match_one(command_line, pos, unmatched, outputs)
            if res is None:
                return
            pos = res
        return pos

# Represents a single input file.
# If id is set, then the file must have been created
# by a prior use of output_file.
# If source is set, then the file must be that source file.
class input_file:
    def __init__(self, id=None, source=None):
        assert((id is None) ^ (source is None))
        self.id = id
        self.source = source
    def check(self, path):
        if path.startswith("-"):
            return
        if self.id is not None:
            try:
                with open(path, "r") as f:
                    data = f.read()
                    if data == make_file_contents(self.id):
                        return True
                    else:
                        return
            except:
                return
        elif self.source is not None:
            if self.source == path:
                return True
            else:
                return
        assert(False)
    def match(self, command_line, pos, outputs):
        if self.check(command_line[pos]):
            return pos + 1

# Matches an output file.
# If the full pattern is matched, The
# file will be created.
class output_file:
    def __init__(self, id):
        self.id = id
    def match(self, command_line, pos, outputs):
        if command_line[pos].startswith("-"):
            return
        outputs.append((command_line[pos], self.id))
        return pos + 1

class arg_file:
    def __init__(self, id):
        self.id = id
    def match(self, command_line, pos, outputs):
        if command_line[pos].startswith("-"):
            return
        if fnmatch.fnmatch(command_line[pos], self.id):
            return pos + 1
        else:
            return

# Matches the directory containing an input_file
class target_path(object):
    def __init__(self, id):
        self.tester = input_file(id=id)
    def match(self, command_line, pos, outputs):
        arg = command_line[pos]
        if arg.startswith("-"):
            return
        try:
            for path in os.listdir(arg):
                if self.tester.check(os.path.join(arg, path)):
                    return pos + 1
        except:
            return

# Matches a single argument, which is composed of a prefix and a path
# for example arguments of the form -ofilename.
class arg(object):
    def __init__(self, prefix, a):
        # The prefix should be a string, a should be target_path or input_file.
        self.prefix = prefix
        self.a = a
    def match(self, command_line, pos, outputs):
        s = command_line[pos]
        if s.startswith(self.prefix) and try_match([s[len(self.prefix):]], 0, self.a, outputs) == 1:
            return pos + 1

# Given a file id, returns a string that will be
# written to the file to allow it to be recognized.
def make_file_contents(id):
    return id

# Matches a single pattern from a list.
# If it succeeds, the matching pattern
# is removed from the list.
# Returns the index after the end of the match
def try_match_one(command_line, pos, patterns, outputs):
    for p in patterns:
        tmp = outputs[:]
        res = try_match(command_line, pos, p, tmp)
        if res is not None:
            outputs[:] = tmp
            patterns.remove(p)
            return res

# returns the end of the match if any
def try_match(command_line, pos, pattern, outputs):
    if pos == len(command_line):
        return
    elif type(pattern) is str:
        if pattern == command_line[pos]:
            return pos + 1
    else:
        return pattern.match(command_line, pos, outputs)

known_patterns = []
program_name = None

# Registers a command
# The arguments should be a sequence of:
# str, ordered, unordered, arg, input_file, output_file, target_path
# kwarg: stdout is text that will be printed on success.
def command(*args, **kwargs):
    global known_patterns
    global program_name
    stdout = kwargs.get("stdout", None)
    pattern = ordered(*args)
    known_patterns += [(pattern, stdout)]
    if program_name is None:
        program_name = args[0]
    else:
        assert(program_name == args[0])

# Use this to filter the recognized commands, based on the properties
# passed to b2.
def allow_properties(*args):
    try:
        return all(a in os.environ["B2_PROPERTIES"].split(" ") for a in args)
    except KeyError:
        return True

# Use this in the stdout argument of command to print the command
# for running another script.
def script(name):
    return os.path.join(os.path.dirname(__file__), "bin", re.sub('\.py$', '', name))

def match(command_line):
    for (p, stdout) in known_patterns:
        outputs = []
        if try_match(command_line, 0, p, outputs) == len(command_line):
            return (stdout, outputs)

# Every mock program should call this after setting up all the commands.
def main():
    command_line = [program_name] + sys.argv[1:]
    result = match(command_line)
    if result is not None:
        (stdout, outputs) = result
        if stdout is not None:
            print(stdout)
        for (file,id) in outputs:
            with open(file, "w") as f:
                f.write(make_file_contents(id))
        exit(0)
    else:
        print("ERROR on command: %s"%(" ".join(command_line)))
        exit(1)

# file should be the name of a file in the same directory
# as this.  Must be called after verify_setup
def verify_file(filename):
    global known_files
    if filename not in known_files:
        known_files.add(filename)
        srcdir = os.path.dirname(__file__)
        execfile(os.path.join(srcdir, filename), {})

def verify_setup():
    """Override the behavior of most module components
    in order to detect whether they are being used correctly."""
    global main
    global allow_properties
    global output_file
    global input_file
    global target_path
    global script
    global command
    global verify_errors
    global output_ids
    global input_ids
    global known_files
    def allow_properties(*args):
        return True
    def main():
        pass
    def output_file(id):
        global output_ids
        global verify_error
        if id in output_ids:
            verify_error("duplicate output_file: %s" % id)
        output_ids.add(id)
    def input_file(id=None, source=None):
        if id is not None:
            input_ids.add(id)
    def target_path(id):
        input_ids.add(id)
    def script(filename):
        verify_file(filename)
    def command(*args, **kwargs):
        pass
    verify_errors = []
    output_ids = set()
    input_ids = set()
    known_files = set()

def verify_error(message):
    global verify_errors
    verify_errors += [message]

def verify_finalize():
    for id in input_ids:
        if not id in output_ids:
            verify_error("Input file does not exist: %s" % id)
    for error in verify_errors:
        print("error: %s" % error)
    if len(verify_errors) != 0:
        return 1
    else:
        return 0

def verify():
    srcdir = os.path.dirname(__file__)
    if srcdir == '':
        srcdir = '.'
    verify_setup()
    for f in os.listdir(srcdir):
        if re.match(r"(gcc|clang|darwin|intel)-.*\.py", f):
            verify_file(f)
    exit(verify_finalize())
