/*
 * Copyright  2002-2004 The Apache Software Foundation
 *
 *  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 hudson.org.apache.tools.ant.taskdefs.cvslib;
// patched to work around http://issues.apache.org/bugzilla/show_bug.cgi?id=38583

import org.apache.tools.ant.Project;
import org.apache.commons.io.FileUtils;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;
import java.util.Map.Entry;
import java.util.TimeZone;
import java.util.logging.Logger;
import java.util.logging.Level;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.io.File;
import java.io.IOException;

/**
 * A class used to parse the output of the CVS log command.
 *
 * @version $Revision: 8895 $ $Date: 2008-04-27 22:01:22 -0700 (Sun, 27 Apr 2008) $
 */
class ChangeLogParser {
    //private static final int GET_ENTRY = 0;
    private static final int GET_FILE = 1;
    private static final int GET_DATE = 2;
    private static final int GET_COMMENT = 3;
    private static final int GET_REVISION = 4;
    private static final int GET_PREVIOUS_REV = 5;
    private static final int GET_SYMBOLIC_NAMES = 6;

    /**
     * input format for dates read in from cvs log.
     *
     * Some users reported that they see different formats,
     * so this is extended from original Ant version to cover different formats.
     */
    private static final SimpleDateFormat[] c_inputDate
        = new SimpleDateFormat[]{
            new SimpleDateFormat("yyyy/MM/dd HH:mm:ss Z"),
            new SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z"),
            new SimpleDateFormat("yyyy/MM/dd HH:mm:ss"),
            new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"),
        };

    static {
        TimeZone utc = TimeZone.getTimeZone("UTC");
        for (SimpleDateFormat df : c_inputDate) {
            df.setTimeZone(utc);
        }
    }

    //The following is data used while processing stdout of CVS command
    private String m_file;
    private String m_fullName;
    private String m_date;
    private String m_author;
    private String m_comment;
    private String m_revision;
    private String m_previousRevision;
    /**
     * All branches available on the current file.
     * Keyed by branch revision prefix (like "1.2.3." if files in the branch have revision numbers like
     * "1.2.3.4") and the value is the branch name.
     */
    private final Map<String,String> branches = new HashMap<String,String>();
    /**
     * True if the log record indicates deletion;
     */
    private boolean m_dead;

    private int m_status = GET_FILE;

    /** rcs entries */
    private final Hashtable<String,CVSEntry> m_entries = new Hashtable<String,CVSEntry>();

    private final ChangeLogTask owner;

    public ChangeLogParser(ChangeLogTask owner) {
        this.owner = owner;
    }

    /**
     * Get a list of rcs entries as an array.
     *
     * @return a list of rcs entries as an array
     */
    CVSEntry[] getEntrySetAsArray() {
        final CVSEntry[] array = new CVSEntry[ m_entries.size() ];
        Enumeration e = m_entries.elements();
        int i = 0;
        while (e.hasMoreElements()) {
            array[i++] = (CVSEntry) e.nextElement();
        }
        return array;
    }

    private boolean dead = false;
    private String previousLine = null;
    
    /**
     * Receive notification about the process writing
     * to standard output.
     */
    public void stdout(final String line) {
        if(dead)
            return;
        try {
            switch(m_status) {
                case GET_FILE:
                    // make sure attributes are reset when
                    // working on a 'new' file.
                    reset();
                    processFile(line);
                    break;
                case GET_SYMBOLIC_NAMES:
                    processSymbolicName(line);
                    break;

                case GET_REVISION:
                    processRevision(line);
                    break;

                case GET_DATE:
                    processDate(line);
                    break;

                case GET_COMMENT:
                    processComment(line);
                    break;

                case GET_PREVIOUS_REV:
                    processGetPreviousRevision(line);
                    break;
            }
        } catch (Exception e) {
            // we don't know how to handle the input any more. don't accept any more input
            dead = true;
        }
    }

    /**
     * Process a line while in "GET_COMMENT" state.
     *
     * @param line the line
     */
    private void processComment(final String line) {
        final String lineSeparator = System.getProperty("line.separator");
        if (line.startsWith("======")) {
            //We have ended changelog for that particular file
            //so we can save it
            final int end
                = m_comment.length() - lineSeparator.length(); //was -1
            m_comment = m_comment.substring(0, end);
            saveEntry();
            m_status = GET_FILE;
        } else if (null != previousLine && previousLine.startsWith("----------------------------")) {
            if (line.startsWith("revision")) {
                final int end
                    = m_comment.length() - lineSeparator.length(); //was -1
                m_comment = m_comment.substring(0, end);
                m_status = GET_PREVIOUS_REV;
                
                processGetPreviousRevision(line);
            } else {
                m_comment += previousLine + lineSeparator + line + lineSeparator;
            }
            
            previousLine = null;
        } else if (line.startsWith("----------------------------")) {
            if (null != previousLine) {
                m_comment += previousLine + lineSeparator;
            }
            previousLine = line;
        } else {
            m_comment += line + lineSeparator;
        }
    }

    /**
     * Process a line while in "GET_FILE" state.
     *
     * @param line the line
     */
    private void processFile(final String line) {
        if (line.startsWith("Working file:")) {
            m_file = line.substring(14, line.length());

            File repo = new File(new File(owner.getDir(), m_file).getParentFile(), "CVS/Repository");
            try {
                String module = FileUtils.readFileToString(repo, null);// not sure what encoding CVS uses.
                String simpleName = m_file.substring(m_file.lastIndexOf('/')+1);
                m_fullName = '/'+module.trim()+'/'+simpleName;
            } catch (IOException e) {
                // failed to read
                LOGGER.log(Level.WARNING, "Failed to read CVS/Repository at "+repo,e);
                m_fullName = null;
            }

            m_status = GET_SYMBOLIC_NAMES;
        }
    }

    /**
     * Obtains the revision name list
     */
    private void processSymbolicName(String line) {
        if (line.startsWith("\t")) {
            line = line.trim();
            int idx = line.lastIndexOf(':');
            if(idx<0) {
                // ???
                return;
            }

            String symbol = line.substring(0,idx);
            Matcher m = DOT_PATTERN.matcher(line.substring(idx + 2));
            if(!m.matches())
                return; // not a branch name

            branches.put(m.group(1)+m.group(3)+'.',symbol);
        } else
        if (line.startsWith("keyword substitution:")) {
            m_status = GET_REVISION;
        }
    }

    private static final Pattern DOT_PATTERN = Pattern.compile("(([0-9]+\\.)+)0\\.([0-9]+)");

    /**
     * Process a line while in "REVISION" state.
     *
     * @param line the line
     */
    private void processRevision(final String line) {
        if (line.startsWith("revision")) {
            m_revision = line.substring(9);
            m_status = GET_DATE;
        } else if (line.startsWith("======")) {
            //There was no revisions in this changelog
            //entry so lets move unto next file
            m_status = GET_FILE;
        }
    }

    /**
     * Process a line while in "DATE" state.
     *
     * @param line the line
     */
    private void processDate(final String line) {
        if (line.startsWith("date:")) {
            int idx = line.indexOf(";");
            m_date = line.substring(6, idx);
            String lineData = line.substring(idx + 1);
            m_author = lineData.substring(10, lineData.indexOf(";"));

            m_status = GET_COMMENT;

            m_dead = lineData.indexOf("state: dead;")!=-1;

            //Reset comment to empty here as we can accumulate multiple lines
            //in the processComment method
            m_comment = "";
        }
    }

    /**
     * Process a line while in "GET_PREVIOUS_REVISION" state.
     *
     * @param line the line
     */
    private void processGetPreviousRevision(final String line) {
        if (!line.startsWith("revision")) {
            throw new IllegalStateException("Unexpected line from CVS: "
                + line);
        }
        m_previousRevision = line.substring(9);

        saveEntry();

        m_revision = m_previousRevision;
        m_status = GET_DATE;
    }

    /**
     * Utility method that saves the current entry.
     */
    private void saveEntry() {
        final String entryKey = m_date + m_author + m_comment;
        CVSEntry entry;
        if (!m_entries.containsKey(entryKey)) {
            entry = new CVSEntry(parseDate(m_date), m_author, m_comment);
            m_entries.put(entryKey, entry);
        } else {
            entry = m_entries.get(entryKey);
        }

        String branch = findBranch(m_revision);

        owner.log("Recorded a change: "+m_date+','+m_author+','+m_revision+"(branch="+branch+"),"+m_comment,Project.MSG_VERBOSE);

        entry.addFile(m_file, m_fullName, m_revision, m_previousRevision, branch, m_dead);
    }

    /**
     * Finds the branch name that matches the revision, or null if not found.
     */
    private String findBranch(String revision) {
        if(revision==null)  return null; // defensive check
        for (Entry<String,String> e : branches.entrySet()) {
            if(revision.startsWith(e.getKey()) && revision.substring(e.getKey().length()).indexOf('.')==-1)
                return e.getValue();
        }
        return null;
    }

    /**
     * Parse date out from expected format.
     *
     * @param date the string holding dat
     * @return the date object or null if unknown date format
     */
    private Date parseDate(String date) {
        for (SimpleDateFormat df : c_inputDate) {
            try {
                return df.parse(date);
            } catch (ParseException e) {
                // try next if one fails
            }
        }

        // nothing worked
        owner.log("Failed to parse "+date+"\n", Project.MSG_ERR);
        //final String message = REZ.getString( "changelog.bat-date.error", date );
        //getContext().error( message );
        return null;
    }

    /**
     * reset all internal attributes except status.
     */
    private void reset() {
        m_file = null;
        m_fullName = null;
        m_date = null;
        m_author = null;
        m_comment = null;
        m_revision = null;
        m_previousRevision = null;
        m_dead = false;
        branches.clear();
    }

    private static final Logger LOGGER = Logger.getLogger(ChangeLogParser.class.getName());
}
