# Copyright 2002 Dave Abrahams
# Distributed under the Boost Software License, Version 1.0.
# (See accompanying file LICENSE.txt or https://www.bfgroup.xyz/b2/LICENSE.txt)

import "class" : new ;
import sequence ;
import set ;
import regex ;
import feature ;
import property ;
import container ;
import string ;


# Transform property-set by applying f to each component property.
#
local rule apply-to-property-set ( f property-set )
{
    local properties = [ feature.split $(property-set) ] ;
    return [ string.join [ $(f) $(properties) ] : / ] ;
}


# Expand the given build request by combining all property-sets which do not
# specify conflicting non-free features. Expects all the project files to
# already be loaded.
#
rule expand-no-defaults ( property-sets * )
{
    # First make all features and subfeatures explicit.
    local expanded-property-sets = [ sequence.transform apply-to-property-set
        feature.expand-subfeatures : $(property-sets) ] ;

    # Now combine all of the expanded property-sets
    local product = [ x-product $(expanded-property-sets) : $(feature-space) ] ;

    return $(product) ;
}


# Update the list of expected conflicts based on the new
# features.
#
local rule remove-conflicts ( conflicts * : features * )
{
    local result ;
    for local c in $(conflicts)
    {
        if ! [ set.intersection [ regex.split $(c) "/" ] : $(features) ]
        {
            result += $(c) ;
        }
    }
    return $(result) ;
}


# Implementation of x-product, below. Expects all the project files to already
# be loaded.
#
local rule x-product-aux ( property-sets + )
{
    local result ;
    local p = [ feature.split $(property-sets[1]) ] ;
    local f = [ set.difference $(p:G) : [ feature.free-features ] ] ;
    local seen ;
    local extra-conflicts ;

    # No conflict with things used at a higher level?
    if ! [ set.intersection $(f) : $(x-product-used) ]
    {
        local x-product-seen ;
        local x-product-conflicts =
            [ remove-conflicts $(x-product-conflicts) : $(f) ] ;
        {
            # Do not mix in any conflicting features.
            local x-product-used = $(x-product-used) $(f) ;

            if $(property-sets[2])
            {
                local rest = [ x-product-aux $(property-sets[2-]) ] ;
                result = $(property-sets[1])/$(rest) ;
            }
            if ! $(x-product-conflicts)
            {
                result ?= $(property-sets[1]) ;
            }
        }

        # If we did not encounter a conflicting feature lower down, do not
        # recurse again.
        if ! [ set.intersection $(f) : $(x-product-seen) ]
            || [ remove-conflicts $(x-product-conflicts) : $(x-product-seen) ]
        {
            property-sets = ;
        }
        else
        {
            # A property is only allowed to be absent if it conflicts
            # with either a higher or lower layer.  We don't need to
            # bother setting this if we already know that we don't need
            # to recurse again.
            extra-conflicts = $(f:J=/) ;
        }

        seen = $(x-product-seen) ;
    }

    if $(property-sets[2])
    {
        # Lower layers expansion must conflict with this
        local x-product-conflicts = $(x-product-conflicts) $(extra-conflicts) ;

        result += [ x-product-aux $(property-sets[2-]) ] ;
    }

    # Note that we have seen these features so that higher levels will recurse
    # again without them set.
    x-product-seen += $(f) $(seen) ;
    return $(result) ;
}


# Return the cross-product of all elements of property-sets, less any that would
# contain conflicting values for single-valued features. Expects all the project
# files to already be loaded.
#
# Formal definition:
# Returns all maximum non-conflicting subsets of property-sets.
# The result is a list of all property-sets p such that
# 1. p is composed by joining a subset of property-sets without removing
#    duplicates
# 2. p contains at most one instance of every single-valued feature
# 3. Adding any additional element of property-sets to p be would
#    violate (2)
local rule x-product ( property-sets * )
{
    if $(property-sets).non-empty
    {
        # Prepare some "scoped globals" that can be used by the implementation
        # function, x-product-aux.
        local x-product-seen x-product-used x-product-conflicts ;
        return [ x-product-aux $(property-sets) : $(feature-space) ] ;
    }
    # Otherwise return empty.
}


# Returns true if either 'v' or the part of 'v' before the first '-' symbol is
# an implicit value. Expects all the project files to already be loaded.
#
local rule looks-like-implicit-value ( v )
{
    if [ feature.is-implicit-value $(v) ]
    {
        return true ;
    }
    else
    {
        local split = [ regex.split $(v) - ] ;
        if [ feature.is-implicit-value $(split[1]) ]
        {
            return true ;
        }
    }
}


# Takes the command line tokens (such as taken from the ARGV rule) and
# constructs a build request from them. Returns a vector of two vectors (where
# "vector" means container.jam's "vector"). First is the set of targets
# specified in the command line, and second is the set of requested build
# properties. Expects all the project files to already be loaded.
#
rule from-command-line ( command-line * )
{
    local targets ;
    local properties ;

    command-line = $(command-line[2-]) ;
    local skip-next = ;
    for local e in $(command-line)
    {
        if $(skip-next)
        {
            skip-next = ;
        }
        else if ! [ MATCH ^(-) : $(e) ]
        {
            # Build request spec either has "=" in it or completely consists of
            # implicit feature values.
            local fs = feature-space ;
            if [ MATCH "(.*=.*)" : $(e) ]
                || [ looks-like-implicit-value $(e:D=) : $(feature-space) ]
            {
                properties += $(e) ;
            }
            else if $(e)
            {
                targets += $(e) ;
            }
        }
        else if [ MATCH "^(-[-ldjfsto])$" : $(e) ]
        {
            skip-next = true ;
        }
    }
    return [ new vector
        [ new vector $(targets) ]
        [ new vector $(properties) ] ] ;
}


# Converts a list of elements of command line build request specification into internal
# form. Expects all the project files to already be loaded.
#
rule convert-command-line-elements ( elements * )
{
    local result ;
    for local e in $(elements)
    {
        result += [ convert-command-line-element $(e) ] ;
    }
    return $(result) ;
}


# Converts one element of command line build request specification into internal
# form.
local rule convert-command-line-element ( e )
{
    local result ;
    local parts = [ regex.split $(e) "/" ] ;
    while $(parts)
    {
        local p = $(parts[1]) ;
        local m = [ MATCH "([^=]*)=(.*)" : $(p) ] ;
        local lresult ;
        local feature ;
        local values ;
        if $(m)
        {
            feature = $(m[1]) ;
            values = [ regex.split $(m[2]) "," ] ;
            lresult = <$(feature)>$(values) ;
        }
        else
        {
            lresult = [ regex.split $(p) "," ] ;
        }

        if $(feature) && free in [ feature.attributes <$(feature)> ]
        {
            # If we have free feature, then the value is everything
            # until the end of the command line token. Slashes in
            # the following string are not taked to mean separation
            # of properties. Commas are also not interpreted specially.
            values = $(values:J=,) ;
            values = $(values) $(parts[2-]) ;
            values = $(values:J=/) ;
            lresult = ;
            # Optional free features will ignore empty value arguments.
            if optional in [ feature.attributes <$(feature)> ]
            {
                for local v in $(values)
                {
                    if $(v)
                    {
                        lresult += <$(feature)>$(v) ;
                    }
                }
            }
            else
            {
                lresult = <$(feature)>$(values) ;
            }
            parts = ;
        }

        if ! [ MATCH (.*-.*) : $(p) ]
        {
            # property.validate cannot handle subfeatures, so we avoid the check
            # here.
            for local p in $(lresult)
            {
                property.validate $(p) : $(feature-space) ;
            }
        }

        if $(lresult)
        {
            if ! $(result)
            {
                result = $(lresult) ;
            }
            else
            {
                result = $(result)/$(lresult) ;
            }
        }

        parts = $(parts[2-]) ;
    }

    return $(result) ;
}


rule __test__ ( )
{
    import assert ;
    import feature ;

    feature.prepare-test build-request-test-temp ;

    import build-request ;
    import build-request : expand-no-defaults : build-request.expand-no-defaults ;
    import errors : try catch ;
    import feature : feature subfeature ;

    feature toolset : gcc msvc borland : implicit ;
    subfeature toolset gcc : version : 2.95.2 2.95.3 2.95.4
      3.0 3.0.1 3.0.2 : optional ;

    feature variant : debug release : implicit composite ;
    feature inlining : on off ;
    feature "include" : : free ;

    feature stdlib : native stlport : implicit ;

    feature runtime-link : dynamic static : symmetric ;

    # Empty build requests should expand to empty.
    assert.result
        : build-request.expand-no-defaults ;

    assert.result
        <toolset>gcc/<toolset-gcc:version>3.0.1/<stdlib>stlport/<variant>debug
        <toolset>msvc/<stdlib>stlport/<variant>debug
        <toolset>msvc/<variant>debug
        : build-request.expand-no-defaults gcc-3.0.1/stlport msvc/stlport msvc debug ;

    assert.result
        <toolset>gcc/<toolset-gcc:version>3.0.1/<stdlib>stlport/<variant>debug
        <toolset>msvc/<variant>debug
        <variant>debug/<toolset>msvc/<stdlib>stlport
        : build-request.expand-no-defaults gcc-3.0.1/stlport msvc debug msvc/stlport ;

    assert.result
        <toolset>gcc/<toolset-gcc:version>3.0.1/<stdlib>stlport/<variant>debug/<inlining>off
        <toolset>gcc/<toolset-gcc:version>3.0.1/<stdlib>stlport/<variant>release/<inlining>off
        : build-request.expand-no-defaults gcc-3.0.1/stlport debug release <inlining>off ;

    assert.result
        <include>a/b/c/<toolset>gcc/<toolset-gcc:version>3.0.1/<stdlib>stlport/<variant>debug/<include>x/y/z
        <include>a/b/c/<toolset>msvc/<stdlib>stlport/<variant>debug/<include>x/y/z
        <include>a/b/c/<toolset>msvc/<variant>debug/<include>x/y/z
        : build-request.expand-no-defaults <include>a/b/c gcc-3.0.1/stlport msvc/stlport msvc debug  <include>x/y/z ;

    local r ;

    try ;
    {
        r = [ build-request.from-command-line bjam gcc/debug runtime-link=dynamic/static ] ;
        build-request.convert-command-line-elements [ $(r).get-at 2 ] ;
    }
    catch \"static\" is not an implicit feature value ;

    r = [ build-request.from-command-line bjam debug runtime-link=dynamic ] ;
    assert.equal [ $(r).get-at 1 ] : ;
    assert.equal [ $(r).get-at 2 ] : debug runtime-link=dynamic ;

    assert.equal
        [ build-request.convert-command-line-elements debug runtime-link=dynamic ]
        : debug <runtime-link>dynamic ;

    r = [ build-request.from-command-line bjam -d2 --debug debug target runtime-link=dynamic ] ;
    assert.equal [ $(r).get-at 1 ] : target ;
    assert.equal [ $(r).get-at 2 ] : debug runtime-link=dynamic ;

    assert.equal
        [ build-request.convert-command-line-elements debug runtime-link=dynamic ]
        : debug <runtime-link>dynamic ;

    r = [ build-request.from-command-line bjam debug runtime-link=dynamic,static ] ;
    assert.equal [ $(r).get-at 1 ] : ;
    assert.equal [ $(r).get-at 2 ] : debug runtime-link=dynamic,static ;

    assert.equal
        [ build-request.convert-command-line-elements debug runtime-link=dynamic,static ]
        : debug <runtime-link>dynamic <runtime-link>static ;

    r = [ build-request.from-command-line bjam debug gcc/runtime-link=dynamic,static ] ;
    assert.equal [ $(r).get-at 1 ] : ;
    assert.equal [ $(r).get-at 2 ] : debug gcc/runtime-link=dynamic,static ;

    assert.equal
        [ build-request.convert-command-line-elements debug gcc/runtime-link=dynamic,static ]
        : debug gcc/<runtime-link>dynamic gcc/<runtime-link>static ;

    r = [ build-request.from-command-line bjam msvc gcc,borland/runtime-link=static ] ;
    assert.equal [ $(r).get-at 1 ] : ;
    assert.equal [ $(r).get-at 2 ] : msvc gcc,borland/runtime-link=static ;

    assert.equal
        [ build-request.convert-command-line-elements msvc gcc,borland/runtime-link=static ]
        : msvc gcc/<runtime-link>static borland/<runtime-link>static ;

    r = [ build-request.from-command-line bjam gcc-3.0 ] ;
    assert.equal [ $(r).get-at 1 ] : ;
    assert.equal [ $(r).get-at 2 ] : gcc-3.0 ;

    assert.equal
        [ build-request.convert-command-line-elements gcc-3.0 ]
        : gcc-3.0 ;

    feature.finish-test build-request-test-temp ;
}
