/*
 * 
 * Copyright 2001-2005 The Ant-Contrib project
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */
package net.sf.antcontrib.cpptasks.compiler;

import java.io.File;
import java.io.IOException;
import java.util.Enumeration;
import java.util.Vector;

import net.sf.antcontrib.cpptasks.AssemblerDef;
import net.sf.antcontrib.cpptasks.CCTask;
import net.sf.antcontrib.cpptasks.CUtil;
import net.sf.antcontrib.cpptasks.ProcessorDef;
import net.sf.antcontrib.cpptasks.TargetDef;
import net.sf.antcontrib.cpptasks.types.CommandLineArgument;

import org.apache.tools.ant.BuildException;

/**
 * An abstract Assembler implementation which uses an external program to
 * perform the assemble.
 * 
 */
public abstract class CommandLineAssembler extends AbstractAssembler {

    private String command;

    private String identifier;

    private String identifierArg;

    protected CommandLineAssembler (String command, String identifierArg,
                    String[] sourceExtensions, String[] headerExtensions,
                    String outputSuffix) {
        super(sourceExtensions, headerExtensions, outputSuffix);
        this.command = command;
        this.identifierArg = identifierArg;
    }

    abstract protected void addImpliedArgs(Vector args, boolean debug,
                    Boolean defaultflag);

    /**
     * Adds command-line arguments for include directories.
     * 
     * If relativeArgs is not null will add corresponding relative paths include
     * switches to that vector (for use in building a configuration identifier
     * that is consistent between machines).
     * 
     * @param baseDirPaths
     *            A vector containing the parts of the working directory,
     *            produced by CUtil.DecomposeFile.
     * @param includeDirs
     *            Array of include directory paths
     * @param args
     *            Vector of command line arguments used to execute the task
     * @param relativeArgs
     *            Vector of command line arguments used to build the
     *            configuration identifier
     */
    protected void addIncludes(String baseDirPath, File[] includeDirs,
                    Vector args, Vector relativeArgs, StringBuffer includePathId) {
        for (int i = 0; i < includeDirs.length; i++) {
            args.addElement(getIncludeDirSwitch(includeDirs[i]
                            .getAbsolutePath()));
            if (relativeArgs != null) {
                String relative = CUtil.getRelativePath(baseDirPath,
                                includeDirs[i]);
                relativeArgs.addElement(getIncludeDirSwitch(relative));
                if (includePathId != null) {
                    if (includePathId.length() == 0) {
                        includePathId.append("/I");
                    } else {
                        includePathId.append(" /I");
                    }
                    includePathId.append(relative);
                }
            }
        }
    }

    abstract protected String getIncludeDirSwitch(String source);

    /**
     * Assembles a source file
     * 
     */
    public void assembler(CCTask task, File outputDir, String[] sourceFiles,
                    String[] args, String[] endArgs) throws BuildException {
        String command = getCommand();
        int baseLength = command.length() + args.length + endArgs.length;
        for (int i = 0; i < args.length; i++) {
            baseLength += args[i].length();
        }
        for (int i = 0; i < endArgs.length; i++) {
            baseLength += endArgs[i].length();
        }
        if (baseLength > getMaximumCommandLength()) {
            throw new BuildException(
                            "Command line is over maximum length without sepcifying source file");
        }
        int maxInputFilesPerCommand = getMaximumInputFilesPerCommand();
        int argumentCountPerInputFile = getArgumentCountPerInputFIle();
        for (int sourceIndex = 0; sourceIndex < sourceFiles.length;) {
            int cmdLength = baseLength;
            int firstFileNextExec;
            for (firstFileNextExec = sourceIndex; firstFileNextExec < sourceFiles.length
                            && (firstFileNextExec - sourceIndex) < maxInputFilesPerCommand; firstFileNextExec++) {
                cmdLength += getTotalArgumentLengthForInputFile(outputDir,
                                sourceFiles[firstFileNextExec]);
                if (cmdLength >= getMaximumCommandLength())
                    break;
            }
            if (firstFileNextExec == sourceIndex) {
                throw new BuildException(
                                "Extremely long file name, can't fit on command line");
            }
            int argCount = args.length + 1 + endArgs.length
                            + (firstFileNextExec - sourceIndex)
                            * argumentCountPerInputFile;
            String[] commandline = new String[argCount];
            int index = 0;
            commandline[index++] = command;
            for (int j = 0; j < args.length; j++) {
                commandline[index++] = args[j];
            }
            for (int j = sourceIndex; j < firstFileNextExec; j++) {
                for (int k = 0; k < argumentCountPerInputFile; k++) {
                    commandline[index++] = getInputFileArgument(outputDir,
                                    sourceFiles[j], k);
                }
            }
            for (int j = 0; j < endArgs.length; j++) {
                commandline[index++] = endArgs[j];
            }
            int retval = runCommand(task, outputDir, commandline);
            // if with monitor, add more code
            if (retval != 0) {
                throw new BuildException(this.getCommand()
                                + " failed with return code " + retval, task
                                .getLocation());
            }
            sourceIndex = firstFileNextExec;
        }
    }

    protected AssemblerConfiguration createConfiguration(final CCTask task,
                    final LinkType linkType, final ProcessorDef[] baseDefs,
                    final AssemblerDef specificDef,
                    final TargetDef targetPlatform) {
        Vector args = new Vector();
        AssemblerDef[] defaultProviders = new AssemblerDef[baseDefs.length + 1];
        for (int i = 0; i < baseDefs.length; i++) {
            defaultProviders[i + 1] = (AssemblerDef) baseDefs[i];
        }
        defaultProviders[0] = specificDef;
        Vector cmdArgs = new Vector();
        //
        // add command line arguments inherited from <cc> element
        // any "extends" and finally and specific AssemblerDef
        //
        CommandLineArgument[] commandArgs;
        for (int i = defaultProviders.length - 1; i >= 0; i--) {
            commandArgs = defaultProviders[i].getActiveProcessorArgs();
            for (int j = 0; j < commandArgs.length; j++) {
                if (commandArgs[j].getLocation() == 0) {
                    args.addElement(commandArgs[j].getValue());
                } else {
                    cmdArgs.addElement(commandArgs[j]);
                }
            }
        }
        // omit param
        boolean debug = specificDef.getDebug(baseDefs, 0);
        Boolean defaultflag = specificDef.getDefaultflag(defaultProviders, 1);
        this.addImpliedArgs(args, debug, defaultflag);
        //
        // Want to have distinct set of arguments with relative
        // path names for includes that are used to build
        // the configuration identifier
        //
        Vector relativeArgs = (Vector) args.clone();
        //
        // add all active include an
        //
        StringBuffer includePathIdentifier = new StringBuffer();
        File baseDir = specificDef.getProject().getBaseDir();
        String baseDirPath;
        try {
            baseDirPath = baseDir.getCanonicalPath();
        } catch (IOException ex) {
            baseDirPath = baseDir.toString();
        }
        Vector includePath = new Vector();
        Vector sysIncludePath = new Vector();
        for (int i = defaultProviders.length - 1; i >= 0; i--) {
            String[] incPath = defaultProviders[i].getActiveIncludePaths();
            for (int j = 0; j < incPath.length; j++) {
                includePath.addElement(incPath[j]);
            }
            incPath = defaultProviders[i].getActiveSysIncludePaths();
            for (int j = 0; j < incPath.length; j++) {
                sysIncludePath.addElement(incPath[j]);
            }
        }
        File[] incPath = new File[includePath.size()];
        for (int i = 0; i < includePath.size(); i++) {
            incPath[i] = new File((String) includePath.elementAt(i));
        }
        File[] sysIncPath = new File[sysIncludePath.size()];
        for (int i = 0; i < sysIncludePath.size(); i++) {
            sysIncPath[i] = new File((String) sysIncludePath.elementAt(i));
        }
        addIncludes(baseDirPath, incPath, args, relativeArgs,
                        includePathIdentifier);
        addIncludes(baseDirPath, sysIncPath, args, null, null);
        StringBuffer buf = new StringBuffer(getIdentifier());
        for (int i = 0; i < relativeArgs.size(); i++) {
            buf.append(relativeArgs.elementAt(i));
            buf.append(' ');
        }
        buf.setLength(buf.length() - 1);
        Enumeration argEnum = cmdArgs.elements();
        int endCount = 0;
        while (argEnum.hasMoreElements()) {
            CommandLineArgument arg = (CommandLineArgument) argEnum
                            .nextElement();
            switch (arg.getLocation()) {
            case 1:
                args.addElement(arg.getValue());
                break;
            case 2:
                endCount++;
                break;
            }
        }
        String[] endArgs = new String[endCount];
        argEnum = cmdArgs.elements();
        int index = 0;
        while (argEnum.hasMoreElements()) {
            CommandLineArgument arg = (CommandLineArgument) argEnum
                            .nextElement();
            if (arg.getLocation() == 2) {
                endArgs[index++] = arg.getValue();
            }
        }
        String[] argArray = new String[args.size()];
        args.copyInto(argArray);
        return new CommandLineAssemblerConfiguration(this, incPath, sysIncPath,
                        new File[0], argArray, true, endArgs, new String[0]);
    }

    protected int getArgumentCountPerInputFile() {
        return 1;
    }

    protected abstract File[] getEnvironmentIncludePath();

    public String getIdentifier() {
        if (identifier == null) {
            if (identifierArg == null) {
                identifier = getIdentifier(new String[] { command }, command);
            } else {
                identifier = getIdentifier(new String[] { command,
                                identifierArg }, command);
            }
        }
        return identifier;
    }

    public final String getCommand() {
        return command;
    }

    abstract public int getMaximumCommandLength();

    public void setCommand(String command) {
        this.command = command;
    }

    protected int getTotalArgumentLengthForInputFile(File outputDir,
                    String inputFile) {
        return inputFile.length() + 1;
    }

    protected int runCommand(CCTask task, File workingDir, String[] cmdline)
                    throws BuildException {
        return CUtil.runCommand(task, workingDir, cmdline, false, null);
    }

    protected int getMaximumInputFilesPerCommand() {
        return Integer.MAX_VALUE;
    }

    protected int getArgumentCountPerInputFIle() {
        return 1;
    }

    protected String getInputFileArgument(File outputDir, String filename,
                    int index) {
        //
        // if there is an embedded space,
        // must enclose in quotes
        if (filename.indexOf(' ') >= 0) {
            StringBuffer buf = new StringBuffer("\"");
            buf.append(filename);
            buf.append("\"");
            return buf.toString();
        }
        return filename;
    }
}