# Copyright Rene Rivera 2015-2023
# 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)

# Defines rules that provide requirements based on checking
# conditions using Boost Predef definitions and version numbers.

import modules ;
import project ;
import feature ;
import string ;
import toolset ;
import modules ;
import path ;
import "class" : new ;
import regex ;

# Create a project for our targets.
project.extension predef check ;

# Feature to pass check expressions to check programs.
feature.feature predef-expression : : free ;

# Checks the expressions and when used evaluates to the true-properties
# if the expressions are all true. Otherwise evaluates to the
# false-properties.
rule check ( expressions + : language ? : true-properties * : false-properties * )
{
    # Default to C++ on the check context.
    language ?= cpp ;
    
    local project_target = [ project.target $(__name__) ] ;
    $(project_target).reset-alternatives ;
	project.push-current $(project_target) ;
    local terms ;
    local result ;
    for expression in $(expressions)
    {
        if $(expression:L) in "and" "or"
        {
            terms += $(expression:L) ;
        }
        else
        {
            # Create the check run if we don't have one yet.
            local key = [ MD5 "$(language)::$(expression)" ] ;
            if ! ( $(key) in $(_checks_) )
            {
                _checks_ += $(key) ;
                _message_(/check/predef//predef_check_cc_$(key)) = $(expression) ;
                check_target $(language) $(key) : [ change_term_to_def $(expression) ] ;
            }
            
            terms += /check/predef//predef_check_cc_$(key) ;
        }
    }
    local instance = [ new check-expression-evaluator
        $(terms) : $(true-properties) : $(false-properties) ] ;
    result = <conditional>@$(instance).check ;
    project.pop-current ;
    return $(result) ;
}

# Checks the expressions and when used evaluates to <build>no
# if the expressions are all false. Otherwise evaluates to the
# nothing.
rule require ( expressions + : language ? )
{
    return [ check $(expressions) : $(language) : : <build>no ] ;
}

# Set the address-model to the default/deduced value for the context.
rule address-model ( )
{
    return
        [ check "BOOST_ARCH_WORD_BITS == 0.0.16" : : <address-model>16 ]
        [ check "BOOST_ARCH_WORD_BITS == 0.0.32" : : <address-model>32 ]
        [ check "BOOST_ARCH_WORD_BITS == 0.0.64" : : <address-model>64 ]
        ;
}

# Set the architecture to the default/deduced value to the context.
rule architecture ( )
{
    local result = ;
    local deducible-arch =
        x86 ia64 sparc loongarch mips parisc arm riscv ;
    for arch in $(deducible-arch)
    {
        result += [ check BOOST_ARCH_$(arch:U) : : <architecture>$(arch) ] ;
    }
    result += [ check BOOST_ARCH_PPC : : <architecture>power ] ;
    result += [ check BOOST_ARCH_SYS390 : : <architecture>s390x ] ;
    return $(result) ;
}

#############################################################################

.c.ext = c ;
.cpp.ext = cpp ;
.objc.ext = m ;
.objcpp.ext = mm ;

# Check targets. Each needs to be compiled for different languages
# even though they are all the same source code.
local rule check_target ( language key : requirements * )
{
    # Need to use absolute paths because we don't know the
    # context of the invocation which affects where the paths
    # originate from.
    local predef_jam
        = [ modules.binding $(__name__) ] ;
    local source_path
        = $(predef_jam:D)/predef_check_cc_as_$(language).$(.$(language).ext) ;
    local include_path
        = $(predef_jam:D)/../../include ;
    obj predef_check_cc_$(key)
        : $(source_path)
        : <include>$(include_path) $(requirements) ;
    explicit predef_check_cc_$(key) ;
    return predef_check_cc_$(key) ;
}

local rule change_term_to_def ( term )
{
    local parts = [ regex.split $(term) " " ] ;
    if $(parts[3])
    {
        local version_number = [ regex.split $(parts[3]) "[.]" ] ;
        if ! $(version_number[2]) { version_number += "0" ; }
        if ! $(version_number[3]) { version_number += "0" ; }
        parts = $(parts[1-2]) BOOST_VERSION_NUMBER($(version_number:J=",")) ;
    }
    return <define>CHECK=\"$(parts:J=" ")\" ;
}

class check-expression-evaluator
{
    import configure ;
    import property ;
    
    rule __init__ ( expression + : true-properties * : false-properties * )
    {
        self.expression = $(expression) ;
        self.true-properties = $(true-properties) ;
        self.false-properties = $(false-properties) ;
    }
    
    rule check ( properties * )
    {
        local to-eval ;
        local tokens = "and" "or" ;
        # Go through the expression and: eval the target values,
        # and normalize to a full expression.
        for local term in $(self.expression)
        {
            if ! ( $(term:L) in $(tokens) )
            {
                # A value is a target reference that will evan to "true"
                # or "false".
                if $(to-eval[-1]:L) && ! ( $(to-eval[-1]:L) in $(tokens) )
                {
                    # Default to "and" operation.
                    to-eval += "and" ;
                }
                local message = [ modules.peek predef : _message_($(term)) ] ;
                if [ configure.builds $(term) : $(properties) : $(message) ]
                {
                    to-eval += "true" ;
                }
                else
                {
                    to-eval += "false" ;
                }
            }
            else
            {
                to-eval += $(term) ;
            }
        }
        # Eval full the expression.
        local eval-result = [ eval $(to-eval) ] ;
        # And resolve true/false properties.
        local result = ;
        if $(eval-result) = "true"
        {
            result = $(self.true-properties) ;
        }
        else
        {
            result = $(self.false-properties) ;
        }
        if $(result)
        {
            # Don't override anything that is explicitly specified.
            local base = [ property.select $(result:G) : $(properties) ] ;
            result = [ property.refine $(result) : $(base) ] ;
        }
        return $(result) ;
    }
    
    rule eval ( e * )
    {
        local r ;
        if $(e[1]) && $(e[2]) && $(e[3])
        {
            if $(e[2]) = "and"
            {
                if $(e[1]) = "true" && $(e[3]) = "true"
                {
                    r = [ eval "true" $(e[4-]) ] ;
                }
                else
                {
                    r = [ eval "false" $(e[4-]) ] ;
                }
            }
            else if $(e[2]) = "or"
            {
                if $(e[1]) = "true" || $(e[3]) = "true"
                {
                    r = [ eval "true" $(e[4-]) ] ;
                }
                else
                {
                    r = [ eval "false" $(e[4-]) ] ;
                }
            }
        }
        else
        {
            r = $(e[1]) ;
        }
        return $(r) ;
    }
}
