/*
 * 
 * Copyright 2002-2004 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;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.UnsupportedEncodingException;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Vector;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import net.sf.antcontrib.cpptasks.compiler.CompilerConfiguration;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Project;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;
/**
 * @author Curt Arnold
 */
public final class DependencyTable {
    /**
     * This class handles populates the TargetHistory hashtable in response to
     * SAX parse events
     */
    private class DependencyTableHandler extends DefaultHandler {
        private File baseDir;
        private final DependencyTable dependencyTable;
        private String includePath;
        private Vector includes;
        private String source;
        private long sourceLastModified;
        private Vector sysIncludes;
        /**
         * Constructor
         * 
         * @param history
         *            hashtable of TargetHistory keyed by output name
         * @param outputFiles
         *            existing files in output directory
         */
        private DependencyTableHandler(DependencyTable dependencyTable,
                File baseDir) {
            this.dependencyTable = dependencyTable;
            this.baseDir = baseDir;
            includes = new Vector();
            sysIncludes = new Vector();
            source = null;
        }
        public void endElement(String namespaceURI, String localName,
                String qName) throws SAXException {
            //
            //   if </source> then
            //       create Dependency object and add to hashtable
            //           if corresponding source file exists and
            //           has the same timestamp
            //
            if (qName.equals("source")) {
                if (source != null && includePath != null) {
                    File existingFile = new File(baseDir, source);
                    //
                    //   if the file exists and the time stamp is right
                    //       preserve the dependency info
                    if (existingFile.exists()) {
                        //
                        //   would have expected exact matches
                        //       but was seeing some unexpected difference by
                        //       a few tens of milliseconds, as long
                        //       as the times are within a second
                        long existingLastModified = existingFile.lastModified();
                        long diff = existingLastModified - sourceLastModified;
                        if (diff >= -500 && diff <= 500) {
                            DependencyInfo dependInfo = new DependencyInfo(
                                    includePath, source, sourceLastModified,
                                    includes, sysIncludes);
                            dependencyTable.putDependencyInfo(source,
                                    dependInfo);
                        }
                    }
                    source = null;
                    includes.setSize(0);
                }
            } else {
                //
                //    this causes any <source> elements outside the
                //       scope of an <includePath> to be discarded
                //
                if (qName.equals("includePath")) {
                    includePath = null;
                }
            }
        }
        /**
         * startElement handler
         */
        public void startElement(String namespaceURI, String localName,
                String qName, Attributes atts) throws SAXException {
            //
            //   if includes, then add relative file name to vector
            //
            if (qName.equals("include")) {
                includes.addElement(atts.getValue("file"));
            } else {
                if (qName.equals("sysinclude")) {
                    sysIncludes.addElement(atts.getValue("file"));
                } else {
                    //
                    //    if source then
                    //        capture source file name,
                    //        modification time and reset includes vector
                    //
                    if (qName.equals("source")) {
                        source = atts.getValue("file");
                        sourceLastModified = Long.parseLong(atts
                                .getValue("lastModified"), 16);
                        includes.setSize(0);
                        sysIncludes.setSize(0);
                    } else {
                        if (qName.equals("includePath")) {
                            includePath = atts.getValue("signature");
                        }
                    }
                }
            }
        }
    }
    public abstract class DependencyVisitor {
        /**
         * Previews all the children of this source file.
         * 
         * May be called multiple times as DependencyInfo's for children are
         * filled in.
         * 
         * @return true to continue towards recursion into included files
         */
        public abstract boolean preview(DependencyInfo parent,
                DependencyInfo[] children);
        /**
         * Called if the dependency depth exhausted the stack.
         */
        public abstract void stackExhausted();
        /**
         * Visits the dependency info.
         * 
         * @returns true to continue towards recursion into included files
         */
        public abstract boolean visit(DependencyInfo dependInfo);
    }
    public class TimestampChecker extends DependencyVisitor {
        private boolean noNeedToRebuild;
        private long outputLastModified;
        private boolean rebuildOnStackExhaustion;
        public TimestampChecker(final long outputLastModified,
                boolean rebuildOnStackExhaustion) {
            this.outputLastModified = outputLastModified;
            noNeedToRebuild = true;
            this.rebuildOnStackExhaustion = rebuildOnStackExhaustion;
        }
        public boolean getMustRebuild() {
            return !noNeedToRebuild;
        }
        public boolean preview(DependencyInfo parent, DependencyInfo[] children) {
            int withCompositeTimes = 0;
            long parentCompositeLastModified = parent.getSourceLastModified();
            for (int i = 0; i < children.length; i++) {
                if (children[i] != null) {
                    //
                    //  expedient way to determine if a child forces us to
                    // rebuild
                    //
                    visit(children[i]);
                    long childCompositeLastModified = children[i]
                            .getCompositeLastModified();
                    if (childCompositeLastModified != Long.MIN_VALUE) {
                        withCompositeTimes++;
                        if (childCompositeLastModified > parentCompositeLastModified) {
                            parentCompositeLastModified = childCompositeLastModified;
                        }
                    }
                }
            }
            if (withCompositeTimes == children.length) {
                parent.setCompositeLastModified(parentCompositeLastModified);
            }
            //
            //  may have been changed by an earlier call to visit()
            //
            return noNeedToRebuild;
        }
        public void stackExhausted() {
            if (rebuildOnStackExhaustion) {
                noNeedToRebuild = false;
            }
        }
        public boolean visit(DependencyInfo dependInfo) {
            if (noNeedToRebuild) {
                if (dependInfo.getSourceLastModified() > outputLastModified
                        || dependInfo.getCompositeLastModified() > outputLastModified) {
                    noNeedToRebuild = false;
                }
            }
            //
            //   only need to process the children if
            //      it has not yet been determined whether
            //      we need to rebuild and the composite modified time
            //         has not been determined for this file
            return noNeedToRebuild
                    && dependInfo.getCompositeLastModified() == Long.MIN_VALUE;
        }
    }
    private/* final */File baseDir;
    private String baseDirPath;
    /**
     * a hashtable of DependencyInfo[] keyed by output file name
     */
    private final Hashtable dependencies = new Hashtable();
    /** The file the cache was loaded from. */
    private/* final */File dependenciesFile;
    /** Flag indicating whether the cache should be written back to file. */
    private boolean dirty;
    /**
     * Creates a target history table from dependencies.xml in the prject
     * directory, if it exists. Otherwise, initializes the dependencies empty.
     * 
     * @param task
     *            task used for logging history load errors
     * @param baseDir
     *            output directory for task
     */
    public DependencyTable(File baseDir) {
        if (baseDir == null) {
            throw new NullPointerException("baseDir");
        }
        this.baseDir = baseDir;
        try {
            baseDirPath = baseDir.getCanonicalPath();
        } catch (IOException ex) {
            baseDirPath = baseDir.toString();
        }
        dirty = false;
        //
        //   load any existing dependencies from file
        dependenciesFile = new File(baseDir, "dependencies.xml");
    }
    public void commit(CCTask task) {
        //
        //   if not dirty, no need to update file
        //
        if (dirty) {
            //
            //   walk through dependencies to get vector of include paths
            // identifiers
            //
            Vector includePaths = getIncludePaths();
            //
            //
            //   write dependency file
            //
            try {
                FileOutputStream outStream = new FileOutputStream(
                        dependenciesFile);
                OutputStreamWriter streamWriter;
                //
                //    Early VM's may not have UTF-8 support
                //       fallback to default code page which
                //           "should" be okay unless there are
                //            non ASCII file names
                String encodingName = "UTF-8";
                try {
                    streamWriter = new OutputStreamWriter(outStream, "UTF-8");
                } catch (UnsupportedEncodingException ex) {
                    streamWriter = new OutputStreamWriter(outStream);
                    encodingName = streamWriter.getEncoding();
                }
                BufferedWriter writer = new BufferedWriter(streamWriter);
                writer.write("<?xml version='1.0' encoding='");
                writer.write(encodingName);
                writer.write("'?>\n");
                writer.write("<dependencies>\n");
                StringBuffer buf = new StringBuffer();
                Enumeration includePathEnum = includePaths.elements();
                while (includePathEnum.hasMoreElements()) {
                    writeIncludePathDependencies((String) includePathEnum
                            .nextElement(), writer, buf);
                }
                writer.write("</dependencies>\n");
                writer.close();
                dirty = false;
            } catch (IOException ex) {
                task.log("Error writing " + dependenciesFile.toString() + ":"
                        + ex.toString());
            }
        }
    }
    /**
     * Returns an enumerator of DependencyInfo's
     */
    public Enumeration elements() {
        return dependencies.elements();
    }
    /**
     * This method returns a DependencyInfo for the specific source file and
     * include path identifier
     *  
     */
    public DependencyInfo getDependencyInfo(String sourceRelativeName,
            String includePathIdentifier) {
        DependencyInfo dependInfo = null;
        DependencyInfo[] dependInfos = (DependencyInfo[]) dependencies
                .get(sourceRelativeName);
        if (dependInfos != null) {
            for (int i = 0; i < dependInfos.length; i++) {
                dependInfo = dependInfos[i];
                if (dependInfo.getIncludePathIdentifier().equals(
                        includePathIdentifier)) {
                    return dependInfo;
                }
            }
        }
        return null;
    }
    private Vector getIncludePaths() {
        Vector includePaths = new Vector();
        DependencyInfo[] dependInfos;
        Enumeration dependenciesEnum = dependencies.elements();
        while (dependenciesEnum.hasMoreElements()) {
            dependInfos = (DependencyInfo[]) dependenciesEnum.nextElement();
            for (int i = 0; i < dependInfos.length; i++) {
                DependencyInfo dependInfo = dependInfos[i];
                boolean matchesExisting = false;
                final String dependIncludePath = dependInfo
                        .getIncludePathIdentifier();
                Enumeration includePathEnum = includePaths.elements();
                while (includePathEnum.hasMoreElements()) {
                    if (dependIncludePath.equals(includePathEnum.nextElement())) {
                        matchesExisting = true;
                        break;
                    }
                }
                if (!matchesExisting) {
                    includePaths.addElement(dependIncludePath);
                }
            }
        }
        return includePaths;
    }
    public void load() throws IOException, ParserConfigurationException,
            SAXException {
        dependencies.clear();
        if (dependenciesFile.exists()) {
            SAXParserFactory factory = SAXParserFactory.newInstance();
            factory.setValidating(false);
            SAXParser parser = factory.newSAXParser();
            parser.parse(dependenciesFile, new DependencyTableHandler(this,
                    baseDir));
            dirty = false;
        }
    }
    /**
     * Determines if the specified target needs to be rebuilt.
     * 
     * This task may result in substantial IO as files are parsed to determine
     * their dependencies
     */
    public boolean needsRebuild(CCTask task, TargetInfo target,
            int dependencyDepth) {
        //    look at any files where the compositeLastModified
        //    is not known, but the includes are known
        //
        boolean mustRebuild = false;
        CompilerConfiguration compiler = (CompilerConfiguration) target
                .getConfiguration();
        String includePathIdentifier = compiler.getIncludePathIdentifier();
        File[] sources = target.getSources();
        DependencyInfo[] dependInfos = new DependencyInfo[sources.length];
        long outputLastModified = target.getOutput().lastModified();
        //
        //   try to solve problem using existing dependency info
        //      (not parsing any new files)
        //
        DependencyInfo[] stack = new DependencyInfo[50];
        boolean rebuildOnStackExhaustion = true;
        if (dependencyDepth >= 0) {
            if (dependencyDepth < 50) {
                stack = new DependencyInfo[dependencyDepth];
            }
            rebuildOnStackExhaustion = false;
        }
        TimestampChecker checker = new TimestampChecker(outputLastModified,
                rebuildOnStackExhaustion);
        for (int i = 0; i < sources.length && !mustRebuild; i++) {
            File source = sources[i];
            String relative = CUtil.getRelativePath(baseDirPath, source);
            DependencyInfo dependInfo = getDependencyInfo(relative,
                    includePathIdentifier);
            if (dependInfo == null) {
                task.log("Parsing " + relative, Project.MSG_VERBOSE);
                dependInfo = parseIncludes(task, compiler, source);
            }
            walkDependencies(task, dependInfo, compiler, stack, checker);
            mustRebuild = checker.getMustRebuild();
        }
        return mustRebuild;
    }
    public DependencyInfo parseIncludes(CCTask task,
            CompilerConfiguration compiler, File source) {
        DependencyInfo dependInfo = compiler.parseIncludes(task, baseDir,
                source);
        String relativeSource = CUtil.getRelativePath(baseDirPath, source);
        putDependencyInfo(relativeSource, dependInfo);
        return dependInfo;
    }
    private void putDependencyInfo(String key, DependencyInfo dependInfo) {
        //
        //   optimistic, add new value
        //
        DependencyInfo[] old = (DependencyInfo[]) dependencies.put(key,
                new DependencyInfo[]{dependInfo});
        dirty = true;
        //
        //   something was already there
        //
        if (old != null) {
            //
            //   see if the include path matches a previous entry
            //       if so replace it
            String includePathIdentifier = dependInfo
                    .getIncludePathIdentifier();
            for (int i = 0; i < old.length; i++) {
                DependencyInfo oldDepend = old[i];
                if (oldDepend.getIncludePathIdentifier().equals(
                        includePathIdentifier)) {
                    old[i] = dependInfo;
                    dependencies.put(key, old);
                    return;
                }
            }
            //
            //   no match prepend the new entry to the array
            //      of dependencies for the file
            DependencyInfo[] combined = new DependencyInfo[old.length + 1];
            combined[0] = dependInfo;
            for (int i = 0; i < old.length; i++) {
                combined[i + 1] = old[i];
            }
            dependencies.put(key, combined);
        }
        return;
    }
    public void walkDependencies(CCTask task, DependencyInfo dependInfo,
            CompilerConfiguration compiler, DependencyInfo[] stack,
            DependencyVisitor visitor) throws BuildException {
        //
        //   visit this node
        //       if visit returns true then
        //          visit the referenced include and sysInclude dependencies
        //
        if (visitor.visit(dependInfo)) {
            //
            //   find first null entry on stack
            //
            int stackPosition = -1;
            for (int i = 0; i < stack.length; i++) {
                if (stack[i] == null) {
                    stackPosition = i;
                    stack[i] = dependInfo;
                    break;
                } else {
                    //
                    //   if we have appeared early in the calling history
                    //      then we didn't exceed the criteria
                    if (stack[i] == dependInfo) {
                        return;
                    }
                }
            }
            if (stackPosition == -1) {
                visitor.stackExhausted();
                return;
            }
            //
            //   locate dependency infos
            //
            String[] includes = dependInfo.getIncludes();
            String includePathIdentifier = compiler.getIncludePathIdentifier();
            DependencyInfo[] includeInfos = new DependencyInfo[includes.length];
            for (int i = 0; i < includes.length; i++) {
                DependencyInfo includeInfo = getDependencyInfo(includes[i],
                        includePathIdentifier);
                includeInfos[i] = includeInfo;
            }
            //
            //   preview with only the already available dependency infos
            //
            if (visitor.preview(dependInfo, includeInfos)) {
                //
                //   now need to fill in the missing DependencyInfos
                //
                int missingCount = 0;
                for (int i = 0; i < includes.length; i++) {
                    if (includeInfos[i] == null) {
                        missingCount++;
                        task.log("Parsing " + includes[i], Project.MSG_VERBOSE);
                        // If the include is part of a UNC don't go building a
                        // relative file name.
                        File src = includes[i].startsWith("\\\\") ? new File(
                                includes[i]) : new File(baseDir, includes[i]);
                        DependencyInfo includeInfo = parseIncludes(task,
                                compiler, src);
                        includeInfos[i] = includeInfo;
                    }
                }
                //
                //   if it passes a review the second time
                //      then recurse into all the children
                if (missingCount == 0
                        || visitor.preview(dependInfo, includeInfos)) {
                    //
                    //   recurse into
                    //
                    for (int i = 0; i < includeInfos.length; i++) {
                        DependencyInfo includeInfo = includeInfos[i];
                        walkDependencies(task, includeInfo, compiler, stack,
                                visitor);
                    }
                }
            }
            stack[stackPosition] = null;
        }
    }
    private void writeDependencyInfo(BufferedWriter writer, StringBuffer buf,
            DependencyInfo dependInfo) throws IOException {
        String[] includes = dependInfo.getIncludes();
        String[] sysIncludes = dependInfo.getSysIncludes();
        //
        //   if the includes have not been evaluted then
        //       it is not worth our time saving it
        //       and trying to distiguish between files with
        //       no dependencies and those with undetermined dependencies
        buf.setLength(0);
        buf.append("      <source file=\"");
        buf.append(CUtil.xmlAttribEncode(dependInfo.getSource()));
        buf.append("\" lastModified=\"");
        buf.append(Long.toHexString(dependInfo.getSourceLastModified()));
        buf.append("\">\n");
        writer.write(buf.toString());
        for (int i = 0; i < includes.length; i++) {
            buf.setLength(0);
            buf.append("         <include file=\"");
            buf.append(CUtil.xmlAttribEncode(includes[i]));
            buf.append("\"/>\n");
            writer.write(buf.toString());
        }
        for (int i = 0; i < sysIncludes.length; i++) {
            buf.setLength(0);
            buf.append("         <sysinclude file=\"");
            buf.append(CUtil.xmlAttribEncode(sysIncludes[i]));
            buf.append("\"/>\n");
            writer.write(buf.toString());
        }
        writer.write("      </source>\n");
        return;
    }
    private void writeIncludePathDependencies(String includePathIdentifier,
            BufferedWriter writer, StringBuffer buf) throws IOException {
        //
        //  include path element
        //
        buf.setLength(0);
        buf.append("   <includePath signature=\"");
        buf.append(CUtil.xmlAttribEncode(includePathIdentifier));
        buf.append("\">\n");
        writer.write(buf.toString());
        Enumeration dependenciesEnum = dependencies.elements();
        while (dependenciesEnum.hasMoreElements()) {
            DependencyInfo[] dependInfos = (DependencyInfo[]) dependenciesEnum
                    .nextElement();
            for (int i = 0; i < dependInfos.length; i++) {
                DependencyInfo dependInfo = dependInfos[i];
                //
                //   if this is for the same include path
                //      then output the info
                if (dependInfo.getIncludePathIdentifier().equals(
                        includePathIdentifier)) {
                    writeDependencyInfo(writer, buf, dependInfo);
                }
            }
        }
        writer.write("   </includePath>\n");
    }
}
