# Copyright (C) 2011 Apple Inc. All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1. Redistributions of source code must retain the above copyright
#    notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
#    notice, this list of conditions and the following disclaimer in the
#    documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
# BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
# THE POSSIBILITY OF SUCH DAMAGE.

require "config"
require "ast"
require "backends"
require "parser"
require "transform"

#
# computeSettingsCombinations(ast) -> settingsCombiations
#
# Computes an array of settings maps, where a settings map constitutes
# a configuration for the assembly code being generated. The map
# contains key value pairs where keys are settings names (strings) and
# the values are booleans (true for enabled, false for disabled).
#

def computeSettingsCombinations(ast)
    settingsCombinations = []
    
    def settingsCombinator(settingsCombinations, mapSoFar, remaining)
        if remaining.empty?
            settingsCombinations << mapSoFar
            return
        end
        
        newMap = mapSoFar.dup
        newMap[remaining[0]] = true
        settingsCombinator(settingsCombinations, newMap, remaining[1..-1])
        
        newMap = mapSoFar.dup
        newMap[remaining[0]] = false
        settingsCombinator(settingsCombinations, newMap, remaining[1..-1])
    end
    
    nonBackendSettings = ast.filter(Setting).uniq.collect{ |v| v.name }
    nonBackendSettings.delete_if {
        | setting |
        isBackend? setting
    }
    
    allBackendsFalse = {}
    BACKENDS.each {
        | backend |
        allBackendsFalse[backend] = false
    }
    
    # This will create entries for invalid backends. That's fine. It's necessary
    # because it ensures that generate_offsets_extractor (which knows about valid
    # backends) has settings indices that are compatible with what asm will see
    # (asm doesn't know about valid backends).
    BACKENDS.each {
        | backend |
        map = allBackendsFalse.clone
        map[backend] = true
        settingsCombinator(settingsCombinations, map, nonBackendSettings)
    }
    
    settingsCombinations
end

#
# forSettings(concreteSettings, ast) {
#     | concreteSettings, lowLevelAST, backend | ... }
#
# Determines if the settings combination is valid, and if so, calls
# the block with the information you need to generate code.
#

def forSettings(concreteSettings, ast)
    # Check which architectures this combinator claims to support.
    numClaimedBackends = 0
    selectedBackend = nil
    BACKENDS.each {
        | backend |
        if concreteSettings[backend]
            raise if selectedBackend
            selectedBackend = backend
        end
    }
    
    return unless isValidBackend? selectedBackend
    
    # Resolve the AST down to a low-level form (no macros or conditionals).
    lowLevelAST = ast.resolveSettings(concreteSettings)
    
    yield concreteSettings, lowLevelAST, selectedBackend
end

#
# forEachValidSettingsCombination(ast) {
#     | concreteSettings, ast, backend, index | ... }
#
# forEachValidSettingsCombination(ast, settingsCombinations) {
#     | concreteSettings, ast, backend, index | ... }
#
# Executes the given block for each valid settings combination in the
# settings map. The ast passed into the block is resolved
# (ast.resolve) against the settings.
#
# The first form will call computeSettingsCombinations(ast) for you.
#

def forEachValidSettingsCombination(ast, *optionalSettingsCombinations)
    raise if optionalSettingsCombinations.size > 1
    
    if optionalSettingsCombinations.empty?
        settingsCombinations = computeSettingsCombinations(ast)
    else
        settingsCombinations = optionalSettingsCombiations[0]
    end
    
    settingsCombinations.each_with_index {
        | concreteSettings, index |
        forSettings(concreteSettings, ast) {
            | concreteSettings_, lowLevelAST, backend |
            yield concreteSettings, lowLevelAST, backend, index
        }
    }
end

#
# cppSettingsTest(concreteSettings)
#
# Returns the C++ code used to test if we are in a configuration that
# corresponds to the given concrete settings.
#

def cppSettingsTest(concreteSettings)
    "#if " + concreteSettings.to_a.collect{
        | pair |
        (if pair[1]
             ""
         else
             "!"
         end) + "OFFLINE_ASM_" + pair[0]
    }.join(" && ")
end

#
# isASTErroneous(ast)
#
# Tests to see if the AST claims that there is an error - i.e. if the
# user's code, after settings resolution, has Error nodes.
#

def isASTErroneous(ast)
    not ast.filter(Error).empty?
end

#
# assertConfiguration(concreteSettings)
#
# Emits a check that asserts that we're using the given configuration.
#

def assertConfiguration(concreteSettings)
    $output.puts cppSettingsTest(concreteSettings)
    $output.puts "#else"
    $output.puts "#error \"Configuration mismatch.\""
    $output.puts "#endif"
end

#
# emitCodeInConfiguration(concreteSettings, ast, backend) {
#     | concreteSettings, ast, backend | ... }
#
# Emits all relevant guards to see if the configuration holds and
# calls the block if the configuration is not erroneous.
#

def emitCodeInConfiguration(concreteSettings, ast, backend)
    Label.resetReferenced

    if !$emitWinAsm
        $output.puts cppSettingsTest(concreteSettings)
    else
        if backend == "X86_WIN"
            $output.puts ".MODEL FLAT, C"
        end
        $output.puts "INCLUDE #{File.basename($output.path)}.sym"
        $output.puts "_TEXT SEGMENT"
    end
    
    if isASTErroneous(ast)
        $output.puts "#error \"Invalid configuration.\""
    elsif not WORKING_BACKENDS.include? backend
        $output.puts "#error \"This backend is not supported yet.\""
    else
        yield concreteSettings, ast, backend
    end
    
    if !$emitWinAsm
        $output.puts "#endif"
    else
        $output.puts "_TEXT ENDS"
        $output.puts "END"

        # Write symbols needed by MASM
        File.open("#{File.basename($output.path)}.sym", "w") {
            | outp |
            Label.forReferencedExtern {
                | name |
                outp.puts "EXTERN #{name[1..-1]} : near"
            }
        }
    end
end

#
# emitCodeInAllConfigurations(ast) {
#     | concreteSettings, ast, backend, index | ... }
#
# Emits guard codes for all valid configurations, and calls the block
# for those configurations that are valid and not erroneous.
#

def emitCodeInAllConfigurations(ast)
    forEachValidSettingsCombination(ast) {
        | concreteSettings, lowLevelAST, backend, index |
        $output.puts cppSettingsTest(concreteSettings)
        yield concreteSettings, lowLevelAST, backend, index
        $output.puts "#endif"
    }
end



