/*
	StatCvs - CVS statistics generation 
	Copyright (C) 2002  Lukasz Pekacki <lukasz@pekacki.de>
	http://statcvs.sf.net/
    
	This library is free software; you can redistribute it and/or
	modify it under the terms of the GNU Lesser General Public
	License as published by the Free Software Foundation; either
	version 2.1 of the License, or (at your option) any later version.

	This library is distributed in the hope that it will be useful,
	but WITHOUT ANY WARRANTY; without even the implied warranty of
	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
	Lesser General Public License for more details.

	You should have received a copy of the GNU Lesser General Public
	License along with this library; if not, write to the Free Software
	Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
    
	$RCSfile: FileBuilder.java,v $
	$Date: 2008/04/02 11:22:14 $
*/
package net.sf.statcvs.input;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.logging.Logger;

import net.sf.statcvs.model.Revision;
import net.sf.statcvs.model.VersionedFile;

/**
 * <p>Builds a {@link VersionedFile} with {@link Revision}s from logging data.
 * This class is responsible for deciding if a file or revisions will be
 * included in the report, for translating from CVS logfile data structures
 * to the data structures in the <tt>net.sf.statcvs.model</tt> package, and
 * for calculating the LOC history for the file.</p>
 * 
 * <p>A main goal of this class is to delay the creation of the <tt>VersionedFile</tt>
 * object until all revisions of the file have been collected from the log.
 * We could simply create <tt>VersionedFile</tt> and <tt>Revision</tt>s on the fly
 * as we parse through the log, but this creates a problem if we decide not
 * to include the file after reading several revisions. The creation of a
 * <tt>VersionedFile</tt> or <tt>Revision</tt> can cause many more objects to
 * be created (<tt>Author</tt>, <tt>Directory</tt>, <tt>Commit</tt>), and
 * it would be very hard to get rid of them if we don't want the file. This
 * problem is solved by first collecting all information about one file in
 * this class, and then, with all information present, deciding if we want
 * to create the model instances or not.</p>
 * 
 * @author Richard Cyganiak <richard@cyganiak.de>
 * @author Tammo van Lessen
 * @version $Id: FileBuilder.java,v 1.18 2008/04/02 11:22:14 benoitx Exp $
 */
public class FileBuilder {
    private static Logger logger = Logger.getLogger(FileBuilder.class.getName());

    private final Builder builder;
    private final String name;
    private final boolean isBinary;
    private final List revisions = new ArrayList();
    private RevisionData lastAdded = null;
    private final Map revBySymnames;

    private int locDelta;
    private boolean flagLocalFileNotFound = false;
    private boolean flagUnexpectedLocalRevision = false;
    private boolean flagNoLocalCVSMetadata = false;

    /**
     * Creates a new <tt>FileBuilder</tt>.
     * 
     * @param builder a <tt>Builder</tt> that provides factory services for
     * author and directory instances and line counts.
     * @param name the filename
     * @param isBinary Is this a binary file or not?
     */
    public FileBuilder(final Builder builder, final String name, final boolean isBinary, final Map revBySymnames) {
        this.builder = builder;
        this.name = name;
        this.isBinary = isBinary;
        this.revBySymnames = revBySymnames;

        logger.fine("logging " + name);
    }

    /**
     * Adds a revision to the file. The revisions must be added in the
     * same order as they appear in the CVS logfile, that is, most recent
     * first.
     * 
     * @param data the revision
     */
    public void addRevisionData(final RevisionData data) {
        if (!data.isOnTrunk()) {
            return;
        }
        if (isBinary && !data.isCreation()) {
            data.setLines(0, 0);
        }
        this.revisions.add(data);
        lastAdded = data;

        locDelta += getLOCChange(data);
    }

    /**
     * Creates and returns a {@link VersionedFile} representation of the file.
     * <tt>null</tt> is returned if the file does not meet certain criteria,
     * for example if its filename meets an exclude filter or if it was dead
     * during the entire logging timespan.
     * 
     * @param beginOfLogDate the date of the begin of the log
     * @return a <tt>VersionedFile</tt> representation of the file.
     */
    public VersionedFile createFile(final Date beginOfLogDate) {
        if (isFilteredFile() || !fileExistsInLogPeriod()) {
            return null;
        }
        if (revisions.size() == 1 && lastAdded.isAddOnSubbranch()) {
            return null;
        }

        final VersionedFile file = new VersionedFile(name, builder.getDirectory(name));

        if (revisions.isEmpty()) {
            buildBeginOfLogRevision(file, beginOfLogDate, getFinalLOC(), null);
            return file;
        }

        final Iterator it = revisions.iterator();
        RevisionData currentData = (RevisionData) it.next();
        int currentLOC = getFinalLOC();
        RevisionData previousData;
        int previousLOC;
        SortedSet symbolicNames;

        while (it.hasNext()) {
            previousData = currentData;
            previousLOC = currentLOC;
            currentData = (RevisionData) it.next();
            currentLOC = previousLOC - getLOCChange(previousData);

            // symbolic names for previousData
            symbolicNames = createSymbolicNamesCollection(previousData);

            if (previousData.isChangeOrRestore()) {
                if (currentData.isDeletion() || currentData.isAddOnSubbranch()) {
                    buildCreationRevision(file, previousData, previousLOC, symbolicNames);
                } else {
                    buildChangeRevision(file, previousData, previousLOC, symbolicNames);
                }
            } else if (previousData.isDeletion()) {
                buildDeletionRevision(file, previousData, previousLOC, symbolicNames);
            } else {
                logger.warning("illegal state in " + file.getFilenameWithPath() + ":" + previousData.getRevisionNumber());
            }
        }

        // symbolic names for currentData
        symbolicNames = createSymbolicNamesCollection(currentData);

        final int nextLinesOfCode = currentLOC - getLOCChange(currentData);
        if (currentData.isCreation()) {
            buildCreationRevision(file, currentData, currentLOC, symbolicNames);
        } else if (currentData.isDeletion()) {
            buildDeletionRevision(file, currentData, currentLOC, symbolicNames);
            buildBeginOfLogRevision(file, beginOfLogDate, nextLinesOfCode, symbolicNames);
        } else if (currentData.isChangeOrRestore()) {
            buildChangeRevision(file, currentData, currentLOC, symbolicNames);
            buildBeginOfLogRevision(file, beginOfLogDate, nextLinesOfCode, symbolicNames);
        } else if (currentData.isAddOnSubbranch()) {
            // ignore
        } else {
            logger.warning("illegal state in " + file.getFilenameWithPath() + ":" + currentData.getRevisionNumber());
        }
        return file;
    }

    public boolean hasUnexpectedLocalRevision() {
        return this.flagUnexpectedLocalRevision;
    }

    public boolean hasLocalFileNotFound() {
        return this.flagLocalFileNotFound;
    }

    public boolean hasLocalCVSMetadata() {
        return !this.flagNoLocalCVSMetadata;
    }

    /**
     * Gets a LOC count for the file's most recent revision. If the file
     * exists in the local checkout, we ask the {@link RepositoryFileManager}
     * to count its lines of code. If not (that is, it is dead), return
     * an approximated LOC value for its last non-dead revision.
     *  
     * @return the LOC count for the file's most recent revision.
     */
    private int getFinalLOC() {
        if (isBinary) {
            return 0;
        } else if (lastAdded != null && lastAdded.isAddOnSubbranch()) {
            return locDelta;
        }

        String revision = null;
        try {
            revision = builder.getRevision(name);
        } catch (final IOException e) {
            if (!finalRevisionIsDead()) {
                logger.info(e.getMessage());
                this.flagNoLocalCVSMetadata = true;
            }
            revision = "???";
        }

        try {
            if ("1.1".equals(revision)) {
                return builder.getLOC(name) + locDelta;
            } else {
                if (!revisions.isEmpty()) {
                    final RevisionData firstAdded = (RevisionData) revisions.get(0);
                    if (!finalRevisionIsDead() && !firstAdded.getRevisionNumber().equals(revision)) {
                        if (!"???".equals(revision)) {
                            logger.info(this.name + " should be at " + firstAdded.getRevisionNumber() + " but is at " + revision);
                        }
                        this.flagUnexpectedLocalRevision = true;
                    }
                }
                return builder.getLOC(name);
            }
        } catch (final NoLineCountException e) {
            if (!finalRevisionIsDead()) {
                logger.info("No line count for " + this.name);
                this.flagLocalFileNotFound = true;
            }
            return approximateFinalLOC();
        }
    }

    /**
     * Returns <tt>true</tt> if the file's most recent revision is dead.
     * 
     * @return <tt>true</tt> if the file is dead.
     */
    private boolean finalRevisionIsDead() {
        if (revisions.isEmpty()) {
            return false;
        }
        return ((RevisionData) revisions.get(0)).isDeletion();
    }

    /**
     * Approximates the LOC count for files that are not present in the
     * local checkout. If a file was deleted at some point in history, then
     * we can't count its final lines of code. This algorithm calculates
     * a lower bound for the file's LOC prior to deletion by following the
     * ups and downs of the revisions.
     * 
     * @return a lower bound for the file's LOC before it was deleted
     */
    private int approximateFinalLOC() {
        int max = 0;
        int current = 0;
        final Iterator it = revisions.iterator();
        while (it.hasNext()) {
            final RevisionData data = (RevisionData) it.next();
            current += data.getLinesAdded();
            max = Math.max(current, max);
            current -= data.getLinesRemoved();
        }
        return max;
    }

    /**
     * Returns the change in LOC count caused by a revision. If there were
     * 10 lines added and 3 lines removed, 7 would be returned. This does
     * not take into account file deletion and creation.
     * 
     * @param data a revision
     * @return the change in LOC count
     */
    private int getLOCChange(final RevisionData data) {
        return data.getLinesAdded() - data.getLinesRemoved();
    }

    private void buildCreationRevision(final VersionedFile file, final RevisionData data, final int loc, final SortedSet symbolicNames) {
        file.addInitialRevision(data.getRevisionNumber(), builder.getAuthor(data.getLoginName()), data.getDate(), data.getComment(), loc, symbolicNames);
    }

    private void buildChangeRevision(final VersionedFile file, final RevisionData data, final int loc, final SortedSet symbolicNames) {
        file.addChangeRevision(data.getRevisionNumber(), builder.getAuthor(data.getLoginName()), data.getDate(), data.getComment(), loc, data.getLinesAdded()
                - data.getLinesRemoved(), Math.min(data.getLinesAdded(), data.getLinesRemoved()), symbolicNames);
    }

    private void buildDeletionRevision(final VersionedFile file, final RevisionData data, final int loc, final SortedSet symbolicNames) {
        file.addDeletionRevision(data.getRevisionNumber(), builder.getAuthor(data.getLoginName()), data.getDate(), data.getComment(), loc, symbolicNames);
    }

    private void buildBeginOfLogRevision(final VersionedFile file, final Date beginOfLogDate, final int loc, final SortedSet symbolicNames) {
        final Date date = new Date(beginOfLogDate.getTime() - 60000);
        file.addBeginOfLogRevision(date, loc, symbolicNames);
    }

    /**
     * Takes a filename and checks if it should be processed or not.
     * Can be used to filter out unwanted files.
     * 
     * @return <tt>true</tt> if this file should not be processed
     */
    private boolean isFilteredFile() {
        return !this.builder.matchesPatterns(this.name);
    }

    /**
     * Returns <tt>false</tt> if the file did never exist in the timespan
     * covered by the log. For our purposes, a file is non-existant if it
     * has no revisions and does not exists in the module checkout.
     * Note: A file with no revisions
     * must be included in the report if it does exist in the module checkout.
     * This happens if it was created before the log started, and not changed
     * before the log ended.
     * @return <tt>true</tt> if the file did exist at some point in the log period.
     */
    private boolean fileExistsInLogPeriod() {
        if (revisions.size() > 0) {
            return true;
        }
        try {
            builder.getLOC(name);
            return true;
        } catch (final NoLineCountException fileDoesNotExistInTimespan) {
            return false;
        }
    }

    /**
     * Creates a sorted set containing all symbolic name objects affected by 
     * this revision.
     * If this revision has no symbolic names, this method returns null.
     * 
     * @param revisionData this revision
     * @return the sorted set or null
     */
    private SortedSet createSymbolicNamesCollection(final RevisionData revisionData) {
        SortedSet symbolicNames = null;

        final Iterator symIt = revBySymnames.keySet().iterator();
        while (symIt.hasNext()) {
            final String symName = (String) symIt.next();
            final String rev = (String) revBySymnames.get(symName);
            if (revisionData.getRevisionNumber().equals(rev)) {
                if (symbolicNames == null) {
                    symbolicNames = new TreeSet();
                }
                logger.fine("adding revision " + name + "," + revisionData.getRevisionNumber() + " to symname " + symName);
                symbolicNames.add(builder.getSymbolicName(symName));
            }
        }

        return symbolicNames;
    }
}