# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
from pyparsing import (CharsNotIn, Group, Forward, Literal, Suppress, Word,
                       QuotedString, ZeroOrMore, alphas, alphanums)
from string import Template
import re

# Grammar for CMake
comment = Literal('#') + ZeroOrMore(CharsNotIn('\n'))
quoted_argument = QuotedString('\"', '\\', multiline=True)
unquoted_argument = CharsNotIn('\n ()#\"\\')
argument = quoted_argument | unquoted_argument | Suppress(comment)
arguments = Forward()
arguments << (argument | (Literal('(') + ZeroOrMore(arguments) + Literal(')')))
identifier = Word(alphas, alphanums+'_')
command = Group(identifier + Literal('(') + ZeroOrMore(arguments) + Literal(')'))
file_elements = command | Suppress(comment)
cmake = ZeroOrMore(file_elements)


def extract_arguments(parsed):
    """Extract the command arguments skipping the parentheses"""
    return parsed[2:len(parsed) - 1]


def match_block(command, parsed, start):
    """Find the end of block starting with the command"""
    depth = 0
    end = start + 1
    endcommand = 'end' + command
    while parsed[end][0] != endcommand or depth > 0:
        if parsed[end][0] == command:
            depth += 1
        elif parsed[end][0] == endcommand:
            depth -= 1
        end = end + 1
        if end == len(parsed):
            print('error: eof when trying to match block statement: %s'
                  % parsed[start])
    return end


def parse_if(parsed, start):
    """Parse if/elseif/else/endif into a list of conditions and commands"""
    depth = 0
    conditions = []
    condition = [extract_arguments(parsed[start])]
    start = start + 1
    end = start

    while parsed[end][0] != 'endif' or depth > 0:
        command = parsed[end][0]
        if command == 'if':
            depth += 1
        elif command == 'else' and depth == 0:
            condition.append(parsed[start:end])
            conditions.append(condition)
            start = end + 1
            condition = [['TRUE']]
        elif command == 'elseif' and depth == 0:
            condition.append(parsed[start:end])
            conditions.append(condition)
            condition = [extract_arguments(parsed[end])]
            start = end + 1
        elif command == 'endif':
            depth -= 1
        end = end + 1
        if end == len(parsed):
            print('error: eof when trying to match if statement: %s'
                  % parsed[start])
    condition.append(parsed[start:end])
    conditions.append(condition)
    return end, conditions


def substs(variables, values):
    """Substitute variables into values"""
    new_values = []
    for value in values:
        t = Template(value)
        new_value = t.safe_substitute(variables)

        # Safe substitute leaves unrecognized variables in place.
        # We replace them with the empty string.
        new_values.append(re.sub(r'\$\{\w+\}', '', new_value))
    return new_values


def evaluate(variables, cache_variables, parsed):
    """Evaluate a list of parsed commands, returning sources to build"""
    i = 0
    sources = []
    while i < len(parsed):
        command = parsed[i][0]
        arguments = substs(variables, extract_arguments(parsed[i]))

        if command == 'foreach':
            end = match_block(command, parsed, i)
            for argument in arguments[1:]:
                # ; is also a valid divider, why have one when you can have two?
                argument = argument.replace(';', ' ')
                for value in argument.split():
                    variables[arguments[0]] = value
                    cont_eval, new_sources = evaluate(variables, cache_variables,
                                                      parsed[i+1:end])
                    sources.extend(new_sources)
                    if not cont_eval:
                        return cont_eval, sources
        elif command == 'function':
            # for now we just execute functions inline at point of declaration
            # as this is sufficient to build libaom
            pass
        elif command == 'if':
            i, conditions = parse_if(parsed, i)
            for condition in conditions:
                if evaluate_boolean(variables, condition[0]):
                    cont_eval, new_sources = evaluate(variables,
                                                      cache_variables,
                                                      condition[1])
                    sources.extend(new_sources)
                    if not cont_eval:
                        return cont_eval, sources
                    break
        elif command == 'include':
            if arguments:
                try:
                    print('including: %s' % arguments[0])
                    sources.extend(parse(variables, cache_variables, arguments[0]))
                except IOError:
                    print('warning: could not include: %s' % arguments[0])
        elif command == 'list':
            try:
                action = arguments[0]
                variable = arguments[1]
                values = arguments[2:]
                if action == 'APPEND':
                    if variable not in variables:
                        variables[variable] = ' '.join(values)
                    else:
                        variables[variable] += ' ' + ' '.join(values)
            except (IndexError, KeyError):
                pass
        elif command == 'option':
            variable = arguments[0]
            value = arguments[2]
            # Allow options to be override without changing CMake files
            if variable not in variables:
                variables[variable] = value
        elif command == 'return':
            return False, sources
        elif command == 'set':
            variable = arguments[0]
            values = arguments[1:]
            # CACHE variables are not set if already present
            try:
                cache = values.index('CACHE')
                values = values[0:cache]
                if variable not in variables:
                    variables[variable] = ' '.join(values)
                cache_variables.append(variable)
            except ValueError:
                variables[variable] = ' '.join(values)
        # we need to emulate the behavior of these function calls
        # because we don't support interpreting them directly
        # see bug 1492292
        elif command in ['set_aom_config_var', 'set_aom_detect_var']:
            variable = arguments[0]
            value = arguments[1]
            if variable not in variables:
                variables[variable] = value
            cache_variables.append(variable)
        elif command == 'set_aom_option_var':
            # option vars cannot go into cache_variables
            variable = arguments[0]
            value = arguments[2]
            if variable not in variables:
                variables[variable] = value
        elif command == 'add_asm_library':
            try:
                sources.extend(variables[arguments[1]].split(' '))
            except (IndexError, KeyError):
                pass
        elif command == 'add_intrinsics_object_library':
            try:
                sources.extend(variables[arguments[3]].split(' '))
            except (IndexError, KeyError):
                pass
        elif command == 'add_library':
            for source in arguments[1:]:
                sources.extend(source.split(' '))
        elif command == 'target_sources':
            for source in arguments[1:]:
                sources.extend(source.split(' '))
        elif command == 'MOZDEBUG':
            print('>>>> MOZDEBUG: %s' % ' '.join(arguments))
        i += 1
    return True, sources


def evaluate_boolean(variables, arguments):
    """Evaluate a boolean expression"""
    if not arguments:
        return False

    argument = arguments[0]

    if argument == 'NOT':
        return not evaluate_boolean(variables, arguments[1:])

    if argument == '(':
        i = 0
        depth = 1
        while depth > 0 and i < len(arguments):
            i += 1
            if arguments[i] == '(':
                depth += 1
            if arguments[i] == ')':
                depth -= 1
        return evaluate_boolean(variables, arguments[1:i])

    def evaluate_constant(argument):
        try:
            as_int = int(argument)
            if as_int != 0:
                return True
            else:
                return False
        except ValueError:
            upper = argument.upper()
            if upper in ['ON', 'YES', 'TRUE', 'Y']:
                return True
            elif upper in ['OFF', 'NO', 'FALSE', 'N', 'IGNORE', '', 'NOTFOUND']:
                return False
            elif upper.endswith('-NOTFOUND'):
                return False
        return None

    def lookup_variable(argument):
        # If statements can have old-style variables which are not demarcated
        # like ${VARIABLE}. Attempt to look up the variable both ways.
        try:
            if re.search(r'\$\{\w+\}', argument):
                try:
                    t = Template(argument)
                    value = t.substitute(variables)
                    try:
                        # Attempt an old-style variable lookup with the
                        # substituted value.
                        return variables[value]
                    except KeyError:
                        return value
                except ValueError:
                    # TODO: CMake supports nesting, e.g. ${${foo}}
                    return None
            else:
                return variables[argument]
        except KeyError:
            return None

    lhs = lookup_variable(argument)
    if lhs is None:
        # variable resolution failed, treat as string
        lhs = argument

    if len(arguments) > 1:
        op = arguments[1]
        if op == 'AND':
            return evaluate_constant(lhs) and evaluate_boolean(variables, arguments[2:])
        elif op == 'MATCHES':
            rhs = lookup_variable(arguments[2])
            if not rhs:
                rhs = arguments[2]
            return not re.match(rhs, lhs) is None
        elif op == 'OR':
            return evaluate_constant(lhs) or evaluate_boolean(variables, arguments[2:])
        elif op == 'STREQUAL':
            rhs = lookup_variable(arguments[2])
            if not rhs:
                rhs = arguments[2]
            return lhs == rhs
    else:
        lhs = evaluate_constant(lhs)
        if lhs is None:
            lhs = lookup_variable(argument)

    return lhs


def parse(variables, cache_variables, filename):
    parsed = cmake.parseFile(filename)
    cont_eval, sources = evaluate(variables, cache_variables, parsed)
    return sources
