/*
 * 
 * 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.SAXParser;
import javax.xml.parsers.SAXParserFactory;

import net.sf.antcontrib.cpptasks.compiler.ProcessorConfiguration;

import org.apache.tools.ant.BuildException;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;
/**
 * A history of the compiler and linker settings used to build the files in the
 * same directory as the history.
 * 
 * @author Curt Arnold
 */
public final class TargetHistoryTable {
    /**
     * This class handles populates the TargetHistory hashtable in response to
     * SAX parse events
     */
    private class TargetHistoryTableHandler extends DefaultHandler {
        private final File baseDir;
        private String config;
        private final Hashtable history;
        private String output;
        private long outputLastModified;
        private final Vector sources = new Vector();
        /**
         * Constructor
         * 
         * @param history
         *            hashtable of TargetHistory keyed by output name
         * @param outputFiles
         *            existing files in output directory
         */
        private TargetHistoryTableHandler(Hashtable history, File baseDir) {
            this.history = history;
            config = null;
            output = null;
            this.baseDir = baseDir;
        }
        public void endElement(String namespaceURI, String localName,
                String qName) throws SAXException {
            //
            //   if </target> then
            //       create TargetHistory object and add to hashtable
            //           if corresponding output file exists and
            //           has the same timestamp
            //
            if (qName.equals("target")) {
                if (config != null && output != null) {
                    File existingFile = new File(baseDir, output);
                    //
                    //   if the corresponding files doesn't exist or has a
                    // different
                    //      modification time, then discard this record
                    if (existingFile.exists()) {
                        long existingLastModified = existingFile.lastModified();
                        //
                        //   would have expected exact time stamps
                        //      but have observed slight differences
                        //      in return value for multiple evaluations of
                        //      lastModified(). Check if times are within
                        //      a second
                        long diff = outputLastModified - existingLastModified;
                        if (diff >= -500 && diff <= 500) {
                            SourceHistory[] sourcesArray = new SourceHistory[sources
                                    .size()];
                            sources.copyInto(sourcesArray);
                            TargetHistory targetHistory = new TargetHistory(
                                    config, output, outputLastModified,
                                    sourcesArray);
                            history.put(output, targetHistory);
                        }
                    }
                }
                output = null;
                sources.setSize(0);
            } else {
                //
                //   reset config so targets not within a processor element
                //      don't pick up a previous processors signature
                //
                if (qName.equals("processor")) {
                    config = null;
                }
            }
        }
        /**
         * startElement handler
         */
        public void startElement(String namespaceURI, String localName,
                String qName, Attributes atts) throws SAXException {
            //
            //   if sourceElement
            //
            if (qName.equals("source")) {
                String sourceFile = atts.getValue("file");
                long sourceLastModified = Long.parseLong(atts
                        .getValue("lastModified"), 16);
                sources.addElement(new SourceHistory(sourceFile,
                        sourceLastModified));
            } else {
                //
                //   if <target> element,
                //      grab file name and lastModified values
                //      TargetHistory object will be created in endElement
                //
                if (qName.equals("target")) {
                    sources.setSize(0);
                    output = atts.getValue("file");
                    outputLastModified = Long.parseLong(atts
                            .getValue("lastModified"), 16);
                } else {
                    //
                    //   if <processor> element,
                    //       grab signature attribute
                    //
                    if (qName.equals("processor")) {
                        config = atts.getValue("signature");
                    }
                }
            }
        }
    }
    /** Flag indicating whether the cache should be written back to file. */
    private boolean dirty;
    /**
     * a hashtable of TargetHistory's keyed by output file name
     */
    private final Hashtable history = new Hashtable();
    /** The file the cache was loaded from. */
    private/* final */File historyFile;
    private/* final */File outputDir;
    private String outputDirPath;
    /**
     * Creates a target history table from history.xml in the output directory,
     * if it exists. Otherwise, initializes the history table empty.
     * 
     * @param task
     *            task used for logging history load errors
     * @param outputDir
     *            output directory for task
     */
    public TargetHistoryTable(CCTask task, File outputDir)
            throws BuildException {
        if (outputDir == null) {
            throw new NullPointerException("outputDir");
        }
        if (!outputDir.isDirectory()) {
            throw new BuildException("Output directory is not a directory");
        }
        if (!outputDir.exists()) {
            throw new BuildException("Output directory does not exist");
        }
        this.outputDir = outputDir;
        try {
            outputDirPath = outputDir.getCanonicalPath();
        } catch (IOException ex) {
            outputDirPath = outputDir.toString();
        }
        //
        //   load any existing history from file
        //       suppressing any records whose corresponding
        //       file does not exist, is zero-length or
        //          last modified dates differ
        historyFile = new File(outputDir, "history.xml");
        if (historyFile.exists()) {
            SAXParserFactory factory = SAXParserFactory.newInstance();
            factory.setValidating(false);
            try {
                SAXParser parser = factory.newSAXParser();
                parser.parse(historyFile, new TargetHistoryTableHandler(
                        history, outputDir));
            } catch (Exception ex) {
                //
                //   a failure on loading this history is not critical
                //       but should be logged
                task.log("Error reading history.xml: " + ex.toString());
            }
        } else {
            //
            // create empty history file for identifying new files by last
            // modified
            //   timestamp comperation (to compare with
            //   System.currentTimeMillis() don't work on Unix, because it
            //   maesure timestamps only in seconds).
            //
            try {
                FileOutputStream outputStream = new FileOutputStream(
                        historyFile);
                byte[] historyElement = new byte[]{0x3C, 0x68, 0x69, 0x73,
                        0x74, 0x6F, 0x72, 0x79, 0x2F, 0x3E};
                outputStream.write(historyElement);
                outputStream.close();
            } catch (IOException ex) {
                throw new BuildException("Can't create history file", ex);
            }
        }
    }
    public void commit() throws IOException {
        //
        //   if not dirty, no need to update file
        //
        if (dirty) {
            //
            //   build (small) hashtable of config id's in history
            //
            Hashtable configs = new Hashtable(20);
            Enumeration elements = history.elements();
            while (elements.hasMoreElements()) {
                TargetHistory targetHistory = (TargetHistory) elements
                        .nextElement();
                String configId = targetHistory.getProcessorConfiguration();
                if (configs.get(configId) == null) {
                    configs.put(configId, configId);
                }
            }
            FileOutputStream outStream = new FileOutputStream(historyFile);
            OutputStreamWriter outWriter;
            //
            //   early VM's don't support UTF-8 encoding
            //       try and fallback to the default encoding
            //           otherwise
            String encodingName = "UTF-8";
            try {
                outWriter = new OutputStreamWriter(outStream, "UTF-8");
            } catch (UnsupportedEncodingException ex) {
                outWriter = new OutputStreamWriter(outStream);
                encodingName = outWriter.getEncoding();
            }
            BufferedWriter writer = new BufferedWriter(outWriter);
            writer.write("<?xml version='1.0' encoding='");
            writer.write(encodingName);
            writer.write("'?>\n");
            writer.write("<history>\n");
            StringBuffer buf = new StringBuffer(200);
            Enumeration configEnum = configs.elements();
            while (configEnum.hasMoreElements()) {
                String configId = (String) configEnum.nextElement();
                buf.setLength(0);
                buf.append("   <processor signature=\"");
                buf.append(CUtil.xmlAttribEncode(configId));
                buf.append("\">\n");
                writer.write(buf.toString());
                elements = history.elements();
                while (elements.hasMoreElements()) {
                    TargetHistory targetHistory = (TargetHistory) elements
                            .nextElement();
                    if (targetHistory.getProcessorConfiguration().equals(
                            configId)) {
                        buf.setLength(0);
                        buf.append("      <target file=\"");
                        buf.append(CUtil.xmlAttribEncode(targetHistory
                                .getOutput()));
                        buf.append("\" lastModified=\"");
                        buf.append(Long.toHexString(targetHistory
                                .getOutputLastModified()));
                        buf.append("\">\n");
                        writer.write(buf.toString());
                        SourceHistory[] sourceHistories = targetHistory
                                .getSources();
                        for (int i = 0; i < sourceHistories.length; i++) {
                            buf.setLength(0);
                            buf.append("         <source file=\"");
                            buf.append(CUtil.xmlAttribEncode(sourceHistories[i]
                                    .getRelativePath()));
                            buf.append("\" lastModified=\"");
                            buf.append(Long.toHexString(sourceHistories[i]
                                    .getLastModified()));
                            buf.append("\"/>\n");
                            writer.write(buf.toString());
                        }
                        writer.write("      </target>\n");
                    }
                }
                writer.write("   </processor>\n");
            }
            writer.write("</history>\n");
            writer.close();
            dirty = false;
        }
    }
    public TargetHistory get(String configId, String outputName) {
        TargetHistory targetHistory = (TargetHistory) history.get(outputName);
        if (targetHistory != null) {
            if (!targetHistory.getProcessorConfiguration().equals(configId)) {
                targetHistory = null;
            }
        }
        return targetHistory;
    }
    public void markForRebuild(Hashtable targetInfos) {
        Enumeration targetInfoEnum = targetInfos.elements();
        while (targetInfoEnum.hasMoreElements()) {
            markForRebuild((TargetInfo) targetInfoEnum.nextElement());
        }
    }
    public void markForRebuild(TargetInfo targetInfo) {
        //
        //     if it must already be rebuilt, no need to check further
        //
        if (!targetInfo.getRebuild()) {
            TargetHistory history = get(targetInfo.getConfiguration()
                    .toString(), targetInfo.getOutput().getName());
            if (history == null) {
                targetInfo.mustRebuild();
            } else {
                SourceHistory[] sourceHistories = history.getSources();
                File[] sources = targetInfo.getSources();
                if (sourceHistories.length != sources.length) {
                    targetInfo.mustRebuild();
                } else {
                    for (int i = 0; i < sourceHistories.length
                            && !targetInfo.getRebuild(); i++) {
                        //
                        //   relative file name, must absolutize it on output
                        // directory
                        //
                        boolean foundMatch = false;
                        String historySourcePath = sourceHistories[i]
                                .getAbsolutePath(outputDir);
                        for (int j = 0; j < sources.length; j++) {
                            File targetSource = sources[j];
                            String targetSourcePath = targetSource
                                    .getAbsolutePath();
                            if (targetSourcePath.equals(historySourcePath)) {
                                foundMatch = true;
                                if (targetSource.lastModified() != sourceHistories[i]
                                        .getLastModified()) {
                                    targetInfo.mustRebuild();
                                    break;
                                }
                            }
                        }
                        if (!foundMatch) {
                            targetInfo.mustRebuild();
                        }
                    }
                }
            }
        }
    }
    public void update(ProcessorConfiguration config, String[] sources) {
        String configId = config.getIdentifier();
        String[] onesource = new String[1];
        String outputName;
        for (int i = 0; i < sources.length; i++) {
            onesource[0] = sources[i];
            outputName = config.getOutputFileName(sources[i]);
            update(configId, outputName, onesource);
        }
    }
    private void update(String configId, String outputName, String[] sources) {
        File outputFile = new File(outputDir, outputName);
        //
        //   if output file doesn't exist or predates the start of the
        //        compile step (most likely a compilation error) then
        //        do not write add a history entry
        //
        if (outputFile.exists()
                && outputFile.lastModified() >= historyFile.lastModified()) {
            dirty = true;
            history.remove(outputName);
            SourceHistory[] sourceHistories = new SourceHistory[sources.length];
            for (int i = 0; i < sources.length; i++) {
                File sourceFile = new File(sources[i]);
                long lastModified = sourceFile.lastModified();
                String relativePath = CUtil.getRelativePath(outputDirPath,
                        sourceFile);
                sourceHistories[i] = new SourceHistory(relativePath,
                        lastModified);
            }
            TargetHistory newHistory = new TargetHistory(configId, outputName,
                    outputFile.lastModified(), sourceHistories);
            history.put(outputName, newHistory);
        }
    }
    public void update(TargetInfo linkTarget) {
        File outputFile = linkTarget.getOutput();
        String outputName = outputFile.getName();
        //
        //   if output file doesn't exist or predates the start of the
        //        compile or link step (most likely a compilation error) then
        //        do not write add a history entry
        //
        if (outputFile.exists()
                && outputFile.lastModified() >= historyFile.lastModified()) {
            dirty = true;
            history.remove(outputName);
            SourceHistory[] sourceHistories = linkTarget
                    .getSourceHistories(outputDirPath);
            TargetHistory newHistory = new TargetHistory(linkTarget
                    .getConfiguration().getIdentifier(), outputName, outputFile
                    .lastModified(), sourceHistories);
            history.put(outputName, newHistory);
        }
    }
}
