#!/usr/bin/env python3
#############################################################################
##
## Copyright (C) 2019 The Qt Company Ltd.
## Contact: https://www.qt.io/licensing/
##
## This file is part of the plugins of the Qt Toolkit.
##
## $QT_BEGIN_LICENSE:GPL-EXCEPT$
## Commercial License Usage
## Licensees holding valid commercial Qt licenses may use this file in
## accordance with the commercial license agreement provided with the
## Software or, alternatively, in accordance with the terms contained in
## a written agreement between you and The Qt Company. For licensing terms
## and conditions see https://www.qt.io/terms-conditions. For further
## information use the contact form at https://www.qt.io/contact-us.
##
## GNU General Public License Usage
## Alternatively, this file may be used under the terms of the GNU
## General Public License version 3 as published by the Free Software
## Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
## included in the packaging of this file. Please review the following
## information to ensure the GNU General Public License requirements will
## be met: https://www.gnu.org/licenses/gpl-3.0.html.
##
## $QT_END_LICENSE$
##
#############################################################################


import re
from sympy import simplify_logic, And, Or, Not, SympifyError  # type: ignore
from condition_simplifier_cache import simplify_condition_memoize


def _iterate_expr_tree(expr, op, matches):
    assert expr.func == op
    keepers = ()
    for arg in expr.args:
        if arg in matches:
            matches = tuple(x for x in matches if x != arg)
        elif arg == op:
            (matches, extra_keepers) = _iterate_expr_tree(arg, op, matches)
            keepers = (*keepers, *extra_keepers)
        else:
            keepers = (*keepers, arg)
    return matches, keepers


def _simplify_expressions(expr, op, matches, replacement):
    for arg in expr.args:
        expr = expr.subs(arg, _simplify_expressions(arg, op, matches, replacement))

    if expr.func == op:
        (to_match, keepers) = tuple(_iterate_expr_tree(expr, op, matches))
        if len(to_match) == 0:
            # build expression with keepers and replacement:
            if keepers:
                start = replacement
                current_expr = None
                last_expr = keepers[-1]
                for repl_arg in keepers[:-1]:
                    current_expr = op(start, repl_arg)
                    start = current_expr
                top_expr = op(start, last_expr)
            else:
                top_expr = replacement

            expr = expr.subs(expr, top_expr)

    return expr


def _simplify_flavors_in_condition(base: str, flavors, expr):
    """ Simplify conditions based on the knowledge of which flavors
        belong to which OS. """
    base_expr = simplify_logic(base)
    false_expr = simplify_logic("false")
    for flavor in flavors:
        flavor_expr = simplify_logic(flavor)
        expr = _simplify_expressions(expr, And, (base_expr, flavor_expr), flavor_expr)
        expr = _simplify_expressions(expr, Or, (base_expr, flavor_expr), base_expr)
        expr = _simplify_expressions(expr, And, (Not(base_expr), flavor_expr), false_expr)
    return expr


def _simplify_os_families(expr, family_members, other_family_members):
    for family in family_members:
        for other in other_family_members:
            if other in family_members:
                continue  # skip those in the sub-family

            f_expr = simplify_logic(family)
            o_expr = simplify_logic(other)

            expr = _simplify_expressions(expr, And, (f_expr, Not(o_expr)), f_expr)
            expr = _simplify_expressions(expr, And, (Not(f_expr), o_expr), o_expr)
            expr = _simplify_expressions(expr, And, (f_expr, o_expr), simplify_logic("false"))
    return expr


def _recursive_simplify(expr):
    """ Simplify the expression as much as possible based on
        domain knowledge. """
    input_expr = expr

    # Simplify even further, based on domain knowledge:
    # windowses = ('WIN32', 'WINRT')
    apples = ("MACOS", "UIKIT", "IOS", "TVOS", "WATCHOS")
    bsds = ("FREEBSD", "OPENBSD", "NETBSD")
    androids = ("ANDROID", "ANDROID_EMBEDDED")
    unixes = (
        "APPLE",
        *apples,
        "BSD",
        *bsds,
        "LINUX",
        *androids,
        "HAIKU",
        "INTEGRITY",
        "VXWORKS",
        "QNX",
        "WASM",
    )

    unix_expr = simplify_logic("UNIX")
    win_expr = simplify_logic("WIN32")
    false_expr = simplify_logic("false")
    true_expr = simplify_logic("true")

    expr = expr.subs(Not(unix_expr), win_expr)  # NOT UNIX -> WIN32
    expr = expr.subs(Not(win_expr), unix_expr)  # NOT WIN32 -> UNIX

    # UNIX [OR foo ]OR WIN32 -> ON [OR foo]
    expr = _simplify_expressions(expr, Or, (unix_expr, win_expr), true_expr)
    # UNIX  [AND foo ]AND WIN32 -> OFF [AND foo]
    expr = _simplify_expressions(expr, And, (unix_expr, win_expr), false_expr)

    expr = _simplify_flavors_in_condition("WIN32", ("WINRT",), expr)
    expr = _simplify_flavors_in_condition("APPLE", apples, expr)
    expr = _simplify_flavors_in_condition("BSD", bsds, expr)
    expr = _simplify_flavors_in_condition("UNIX", unixes, expr)
    expr = _simplify_flavors_in_condition("ANDROID", ("ANDROID_EMBEDDED",), expr)

    # Simplify families of OSes against other families:
    expr = _simplify_os_families(expr, ("WIN32", "WINRT"), unixes)
    expr = _simplify_os_families(expr, androids, unixes)
    expr = _simplify_os_families(expr, ("BSD", *bsds), unixes)

    for family in ("HAIKU", "QNX", "INTEGRITY", "LINUX", "VXWORKS"):
        expr = _simplify_os_families(expr, (family,), unixes)

    # Now simplify further:
    expr = simplify_logic(expr)

    while expr != input_expr:
        input_expr = expr
        expr = _recursive_simplify(expr)

    return expr


@simplify_condition_memoize
def simplify_condition(condition: str) -> str:
    input_condition = condition.strip()

    # Map to sympy syntax:
    condition = " " + input_condition + " "
    condition = condition.replace("(", " ( ")
    condition = condition.replace(")", " ) ")

    tmp = ""
    while tmp != condition:
        tmp = condition

        condition = condition.replace(" NOT ", " ~ ")
        condition = condition.replace(" AND ", " & ")
        condition = condition.replace(" OR ", " | ")
        condition = condition.replace(" ON ", " true ")
        condition = condition.replace(" OFF ", " false ")
        # Replace dashes with a token
        condition = condition.replace("-", "_dash_")

    # SymPy chokes on expressions that contain two tokens one next to
    # the other delimited by a space, which are not an operation.
    # So a CMake condition like "TARGET Foo::Bar" fails the whole
    # expression simplifying process.
    # Turn these conditions into a single token so that SymPy can parse
    # the expression, and thus simplify it.
    # Do this by replacing and keeping a map of conditions to single
    # token symbols.
    # Support both target names without double colons, and with double
    # colons.
    pattern = re.compile(r"(TARGET [a-zA-Z]+(?:::[a-zA-Z]+)?)")
    target_symbol_mapping = {}
    all_target_conditions = re.findall(pattern, condition)
    for target_condition in all_target_conditions:
        # Replace spaces and colons with underscores.
        target_condition_symbol_name = re.sub("[ :]", "_", target_condition)
        target_symbol_mapping[target_condition_symbol_name] = target_condition
        condition = re.sub(target_condition, target_condition_symbol_name, condition)

    # Do similar token mapping for comparison operators.
    pattern = re.compile(r"([a-zA-Z_0-9]+ (?:STRLESS|STREQUAL|STRGREATER) [a-zA-Z_0-9]+)")
    comparison_symbol_mapping = {}
    all_comparisons = re.findall(pattern, condition)
    for comparison in all_comparisons:
        # Replace spaces and colons with underscores.
        comparison_symbol_name = re.sub("[ ]", "_", comparison)
        comparison_symbol_mapping[comparison_symbol_name] = comparison
        condition = re.sub(comparison, comparison_symbol_name, condition)

    try:
        # Generate and simplify condition using sympy:
        condition_expr = simplify_logic(condition)
        condition = str(_recursive_simplify(condition_expr))

        # Restore the target conditions.
        for symbol_name in target_symbol_mapping:
            condition = re.sub(symbol_name, target_symbol_mapping[symbol_name], condition)

        # Restore comparisons.
        for comparison in comparison_symbol_mapping:
            condition = re.sub(comparison, comparison_symbol_mapping[comparison], condition)

        # Map back to CMake syntax:
        condition = condition.replace("~", "NOT ")
        condition = condition.replace("&", "AND")
        condition = condition.replace("|", "OR")
        condition = condition.replace("True", "ON")
        condition = condition.replace("False", "OFF")
        condition = condition.replace("_dash_", "-")
    except (SympifyError, TypeError, AttributeError):
        # sympy did not like our input, so leave this condition alone:
        condition = input_condition

    return condition or "ON"
