# Copyright (c) 2010 Vladimir Prus.
# Copyright 2017-2021 Rene Ferdinand Rivera Morell
#
# Use, modification and distribution is subject to the Boost Software
# License Version 1.0. (See accompanying file LICENSE_1_0.txt or
# http://www.boost.org/LICENSE_1_0.txt)

# This module defines function to help with two main tasks:
#
# - Discovering build-time configuration for the purposes of adjusting the build
#   process.
# - Reporting what is built, and how it is configured.

import "class" : new ;
import common ;
import indirect ;
import path ;
import project ;
import property ;
import property-set ;
import targets ;
import config-cache ;
import feature ;
import modules ;
import sequence ;
import utility ;
import virtual-target ;


rule log-summary ( )
{
}


.width = 30 ;

rule set-width ( width )
{
    .width = $(width) ;
}


# Declare that the components specified by the parameter exist.
#
rule register-components ( components * )
{
    .components += $(components) ;
}


# Declare that the components specified by the parameters will be built.
#
rule components-building ( components * )
{
    .built-components += $(components) ;
}


# Report something about component configuration that the user should better
# know.
#
rule log-component-configuration ( component : message )
{
    # FIXME: Implement per-property-set logs.
    .component-logs.$(component) += $(message) ;
}


.variant_index = 0 ;
.nl = "\n" ;

.check_notes = ;

rule log-check-result ( result variant ? )
{
    if ! $(.announced-checks)
    {
        ECHO "Performing configuration checks\n" ;
        .announced-checks = 1 ;
    }

    if $(variant)
    {
        if $(.variant_index.$(variant))
        {
            result = "$(result) [$(.variant_index.$(variant))]" ;
        }
        else
        {
            .variant_index = [ CALC $(.variant_index) + 1 ] ;
            .variant_index.$(variant) = $(.variant_index) ;
            result = "$(result) [$(.variant_index.$(variant))]" ;
            .check_notes += "[$(.variant_index.$(variant))] $(variant)" ;
        }
    }
    # else
    # {
    #     result = "$(result) [?]" ;
    # }

    ECHO $(result) ;
    # FIXME: Unfinished code. Nothing seems to set .check-results at the moment.
    #.check-results += $(result) ;
}


rule log-library-search-result ( library : result variant ? )
{
    local x = [ PAD "    - $(library)" : $(.width) ] ;
    log-check-result "$(x) : $(result)" $(variant) ;
}


rule print-component-configuration ( )
{
    # FIXME: See what was intended with this initial assignment.
    # local c = [ sequence.unique $(.components) ] ;

    ECHO "\nComponent configuration:\n" ;
    local c ;
    for c in $(.components)
    {
        local s ;
        if $(c) in $(.built-components)
        {
            s = "building" ;
        }
        else
        {
            s = "not building" ;
        }
        ECHO [ PAD "    - $(c)" : $(.width) ] ": $(s)" ;
        for local m in $(.component-logs.$(c))
        {
            ECHO "        -" $(m) ;
        }
    }
    ECHO ;
}


rule print-configure-checks-summary ( )
{
    if $(.check_notes)
    {
        ECHO ;
        for local l in $(.check_notes) { ECHO $(l) ; }
    }

    # FIXME: The problem with this approach is that the user sees the checks
    # summary when all checks are done, and has no progress reporting while the
    # checks are being executed.
    if $(.check-results)
    {
        ECHO "Configuration checks summary\n" ;
        for local r in $(.check-results)
        {
            ECHO $(r) ;
        }
        ECHO ;
    }
}

if --reconfigure in [ modules.peek : ARGV ]
{
    .reconfigure = true ;
}

# Handle the --reconfigure option
rule maybe-force-rebuild ( targets * )
{
    if $(.reconfigure)
    {
        local all-targets ;
        for local t in $(targets)
        {
            all-targets += [ virtual-target.traverse $(t) ] ;
        }
        for local t in [ sequence.unique $(all-targets) ]
        {
            $(t).always ;
        }
    }
}

# Attempts to build a set of virtual targets
rule try-build ( targets * : ps : what : retry ? )
{
    local cache-props = [ $(ps).raw ] ;
    local cache-name = $(what) $(cache-props) ;
    cache-name = $(cache-name:J=-) ;
    local value = [ config-cache.get $(cache-name) ] ;
    local cache-min = [ property.as-path [ SORT [ feature.minimize $(cache-props) ] ] ] ;

    local result ;
    local jam-targets ;

    maybe-force-rebuild $(targets) ;

    for local t in $(targets)
    {
        jam-targets += [ $(t).actualize ] ;
    }

    local x ;
    if $(value)
    {
        x = [ PAD "    - $(what)" : $(.width) ] ;
        if $(value) = true
        {
            .$(what)-supported.$(ps) = yes ;
            result = true ;
            x = "$(x) : yes (cached)" ;
        }
        else
        {
            x = "$(x) : no  (cached)" ;
        }
    }
    else if ! UPDATE_NOW in [ RULENAMES ]
    {
        # Cannot determine. Assume existence.
    }
    else
    {
        x = [ PAD "    - $(what)" : $(.width) ] ;
        if [ UPDATE_NOW $(jam-targets) :
             $(.log-fd) : ignore-minus-n : ignore-minus-q ]
        {
            .$(what)-supported.$(ps) = yes ;
            result = true ;
            x = "$(x) : yes" ;
        }
        else
        {
            x = "$(x) : no" ;
        }
    }
    if $(x)
    {
        log-check-result "$(x)" "$(cache-min:J= )" ;
    }
    if ! $(value)
    {
        if $(result)
        {
            config-cache.set $(cache-name) : true ;
        }
        else
        {
            config-cache.set $(cache-name) : false ;
        }
    }
    return $(result) ;
}

# Attempts to build several sets of virtual targets.  Returns the
# the index of the first set that builds.
rule try-find-build ( ps : what : * )
{
    local args = 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 ;
    # The outer layer only needs to check $(what), but we
    # also need to check the individual elements, in case
    # the set of targets has changed since the last build.
    local cache-props = [ $(ps).raw ] ;
    local cache-name = $(what) $($(args)[1]) $(cache-props) ;
    cache-name = $(cache-name:J=-) ;
    local value = [ config-cache.get $(cache-name) ] ;
    local cache-min = [ property.as-path [ SORT [ feature.minimize $(cache-props) ] ] ] ;

    local result ;
    local jam-targets ;

    maybe-force-rebuild $($(args)[2-]) ;

    # Make sure that the targets are always actualized,
    # even if the result is cached.  This is needed to
    # allow clean-all to find them and also to avoid
    # unintentional behavior changes.
    for local t in $($(args)[2-])
    {
        $(t).actualize ;
    }

    if $(value)
    {
        local none = none ; # What to show when the argument
        local name = $(value) ;
        if $(name) != none
        {
            name = [ CALC $(name) + 2 ] ;
        }
        local x = [ PAD "    - $(what)" : $(.width) ] ;
        local y = [ PAD $($(name)[1]) : 3 ] ;
        result = $(value) ;
        log-check-result "$(x) : $(y) (cached)" "$(cache-min:J= )" ;
    }
    else
    {
        local x = [ PAD "    - $(what)" : $(.width) ] ;
        for local i in $(args)
        {
            if ! $($(i)[1])
            {
                break ;
            }
            local jam-targets ;
            for local t in $($(i)[2-])
            {
                jam-targets += [ $(t).actualize ] ;
            }
            if [ UPDATE_NOW $(jam-targets) :
                $(.log-fd) : ignore-minus-n : ignore-minus-q ]
            {
                result = [ CALC $(i) - 2 ] ;
                log-check-result "$(x) : $($(i)[1])" "$(cache-min:J= )" ;
                break ;
            }
        }
        if ! $(result)
        {
            log-check-result "$(x) : none" "$(cache-min:J= )" ;
            result = none ;
        }
    }
    if ! $(value)
    {
        if $(result)
        {
            config-cache.set $(cache-name) : $(result) ;
        }
        else
        {
            config-cache.set $(cache-name) : $(result) ;
        }
    }
    if $(result) != none
    {
        return $(result) ;
    }
}

# Attempt to build a metatarget named by 'metatarget-reference'
# in context of 'project' with properties 'ps'.
# Returns non-empty value if build is OK.
rule builds-raw ( metatarget-reference : project : ps : what : retry ? )
{
    local result ;

    if ! $(retry) && ! $(.$(what)-tested.$(ps))
    {
        .$(what)-tested.$(ps) = true ;

        local targets = [ targets.generate-from-reference
            $(metatarget-reference) : $(project) : $(ps) ] ;

        result = [ try-build $(targets[2-]) : $(ps) : $(what) : $(retry) ] ;
        .$(what)-supported.$(ps) = $(result) ;

        return $(result) ;

    }
    else
    {
        return $(.$(what)-supported.$(ps)) ;
    }
}

# Attempt to build a metatarget named by 'metatarget-reference'
# in context of 'project' with properties 'ps'.
# Returns the 1-based index of the first target
# that builds.
rule find-builds-raw ( project : ps : what : * )
{
    local result ;
    local args = 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 ;

    if ! $(.$(what)-tested.$(ps))
    {
        .$(what)-tested.$(ps) = true ;
        local targets.$(i) what.$(i) ;
        for local i in $(args)
        {
            if ! $($(i))
            {
                break ;
            }
            targets.$(i) = [ targets.generate-from-reference
                $($(i)[1]) : $(project) : $(ps) ] ;
            # ignore usage requirements
            targets.$(i) = $(targets.$(i)[2-]) ;
            if $($(i)[2])
            {
                what.$(i) = $($(i)[2]) ;
            }
            else
            {
                local t = [ targets.resolve-reference
                    $($(i)[1]) : $(project) ] ;
                what.$(i) = [ $(t[1]).name ] ;
            }
        }

        result = [ try-find-build $(ps) : $(what)
            : $(what.4) $(targets.4)
            : $(what.5) $(targets.5)
            : $(what.6) $(targets.6)
            : $(what.7) $(targets.7)
            : $(what.8) $(targets.8)
            : $(what.9) $(targets.9)
            : $(what.10) $(targets.10)
            : $(what.11) $(targets.11)
            : $(what.12) $(targets.12)
            : $(what.13) $(targets.13)
            : $(what.14) $(targets.14)
            : $(what.15) $(targets.15)
            : $(what.16) $(targets.16)
            : $(what.17) $(targets.17)
            : $(what.18) $(targets.18)
            : $(what.19) $(targets.19) ] ;
        .$(what)-result.$(ps) = $(result) ;

        return $(result) ;
    }
    else
    {
        return $(.$(what)-result.$(ps)) ;
    }
}

rule get-relevant-features ( properties * )
{
    local ps-full = [ property-set.create $(properties) ] ;
    local ps-base = [ property-set.create [ $(ps-full).base ] ] ;
    local ps-min = [ feature.expand-subfeatures [ feature.minimize
        [ $(ps-base).raw ] ] ] ;
    local ps-relevant = [ property-set.create $(ps-min) ] ;

    return [ $(ps-relevant).raw ] ;
}

rule builds ( metatarget-reference : properties * : what ? : retry ? )
{
    local relevant = [ get-relevant-features $(properties) ] ;
    local ps = [ property-set.create $(relevant) ] ;
    local t = [ targets.current ] ;
    local p = [ $(t).project ] ;

    if ! $(what)
    {
        local resolved = [ targets.resolve-reference $(metatarget-reference) : $(p) ] ;
        local name = [ $(resolved[1]).name ] ;
        what = "$(name) builds" ;
    }

    return [ builds-raw $(metatarget-reference) : $(p) : $(ps) : $(what) :
        $(retry) ] ;
}

rule find-builds ( what : properties * : * )
{
    local relevant = [ get-relevant-features $(properties) ] ;
    local ps = [ property-set.create $(relevant) ] ;
    local t = [ targets.current ] ;
    local p = [ $(t).project ] ;

    return [ find-builds-raw $(p) : $(ps) : $(what) :
        $(3) : $(4) : $(5) : $(6) : $(7) : $(8) : $(9) :
        $(10) : $(11) : $(12) : $(13) : $(14) : $(15) :
        $(16) : $(17) : $(18) : $(19)  ] ;
}


# Called by B2 startup code to specify the file to receive the
# configuration check results. Should never be called by user code.
#
rule set-log-file ( log-file )
{
    path.makedirs [ path.parent $(log-file) ] ;
    .log-fd = [ FILE_OPEN [ path.native $(log-file) ] : "w" ] ;
    if ! $(.log-fd)
    {
        ECHO "warning:" failed to open log file $(log-file) for writing ;
    }
}


# Frontend rules

class check-target-builds-worker
{
    import configure ;
    import property-set ;
    import targets ;
    import project ;
    import property ;

    rule __init__ ( target message ? : true-properties * : false-properties * )
    {
        local project = [ project.current ] ;
        self.target = $(target) ;
        self.message = $(message) ;
        self.true-properties =
            [ configure.translate-properties $(true-properties) : $(project) ] ;
        self.false-properties =
            [ configure.translate-properties $(false-properties) : $(project) ] ;
    }

    rule check ( properties * )
    {
        local choosen ;
        if [ configure.builds $(self.target) : $(properties) : $(self.message) ]
        {
            choosen = $(self.true-properties) ;
        }
        else
        {
            choosen = $(self.false-properties) ;
        }
        return [ property.evaluate-conditionals-in-context $(choosen) :
            $(properties) ] ;
    }
}

class configure-choose-worker
{
    import configure ;
    import property ;
    import project ;
    rule __init__ ( message : * )
    {
        local project = [ project.current ] ;
        self.message = $(message) ;
        for i in 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
        {
            local name = [ CALC $(i) - 1 ] ;
            self.targets.$(name) = $($(i)[1]) ;
            if ! $($(i)[2]:G) # Check whether the second argument is a property
            {
                self.what.$(name) = $($(i)[2]) ;
                self.props.$(name) = $($(i)[3-]) ;
            }
            else
            {
                self.props.$(name) = $($(i)[2-]) ;
            }
            self.props.$(name) = [ configure.translate-properties
                $(self.props.$(name)) : $(project) ] ;
        }
    }
    rule all-properties ( )
    {
        local i = 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 ;
        return $(self.props.$(i)) ;
    }
    rule check ( properties * )
    {
        local i = [ configure.find-builds $(self.message) : $(properties)
            : $(self.targets.1) $(self.what.1)
            : $(self.targets.2) $(self.what.2)
            : $(self.targets.3) $(self.what.3)
            : $(self.targets.4) $(self.what.4)
            : $(self.targets.5) $(self.what.5)
            : $(self.targets.6) $(self.what.6)
            : $(self.targets.7) $(self.what.7)
            : $(self.targets.8) $(self.what.8)
            : $(self.targets.9) $(self.what.9)
            : $(self.targets.10) $(self.what.10)
            : $(self.targets.11) $(self.what.11)
            : $(self.targets.12) $(self.what.12)
            : $(self.targets.13) $(self.what.13)
            : $(self.targets.14) $(self.what.14)
            : $(self.targets.15) $(self.what.15)
            : $(self.targets.16) $(self.what.16)
            : $(self.targets.17) $(self.what.17)
            : $(self.targets.18) $(self.what.18)
            : $(self.targets.19) $(self.what.19) ] ;
        if $(self.props.$(i))
        {
            return [ property.evaluate-conditionals-in-context $(self.props.$(i)) : $(properties) ] ;
        }
    }
}

rule translate-properties ( properties * : project ? )
{
    if $(project) && [ $(project).location ]
    {
        local location = [ $(project).location ] ;
        local m = [ $(project).project-module ] ;
        local project-id = [ project.attribute $(m) id ] ;
        project-id ?= [ path.root $(location) [ path.pwd ] ] ;
        return [ property.translate $(properties)
          : $(project-id) : $(location) : $(m) ] ;
    }
    else
    {
        return $(properties) ;
    }
}

rule check-target-builds ( target message ? : true-properties * :
    false-properties * )
{
    local instance = [ new check-target-builds-worker $(target) $(message) :
        $(true-properties) : $(false-properties) ] ;
    local rulename = [ indirect.make check : $(instance) ] ;
    return <conditional>@$(rulename)
        [ property.evaluate-conditional-relevance
            $(true-properties) $(false-properties) ] ;
}

# Usage:
# [ configure.choose "architecture"
#     : /config//x86 x86 <architecture>x86
#     : /config//mips mips <architecture>mips
# ]
rule choose ( message : * )
{
    local instance = [ new configure-choose-worker $(message)
        : $(2) : $(3) : $(4) : $(5) : $(6) : $(7) : $(8) : $(9)
        : $(10) : $(11) : $(12) : $(13) : $(14) : $(15) : $(16)
        : $(17) : $(18) : $(19) ] ;
    local rulename = [ indirect.make check : $(instance) ] ;
    return <conditional>@$(rulename)
        [ property.evaluate-conditional-relevance
            [ $(instance).all-properties ] ] ;
}


IMPORT $(__name__) : check-target-builds :  : check-target-builds ;
