/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License (the "License").
 * You may not use this file except in compliance with the License.
 *
 * See LICENSE.txt included in this distribution for the specific
 * language governing permissions and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at LICENSE.txt.
 * If applicable, add the following below this CDDL HEADER, with the
 * fields enclosed by brackets "[]" replaced with your own identifying
 * information: Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 */

/*
 * Copyright (c) 2006, 2014, Oracle and/or its affiliates. All rights reserved.
 */
package org.opensolaris.opengrok.history;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.text.DateFormat;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.logging.Level;
import org.opensolaris.opengrok.OpenGrokLogger;
import org.opensolaris.opengrok.configuration.RuntimeEnvironment;
import org.opensolaris.opengrok.util.Executor;

/**
 * Parse a stream of mercurial log comments.
 */
class MercurialHistoryParser implements Executor.StreamHandler {

    /** Prefix which identifies lines with the description of a commit. */
    private static final String DESC_PREFIX = "description: ";

    private List<HistoryEntry> entries = new ArrayList<HistoryEntry>();
    private final MercurialRepository repository;
    private final String mydir;
    private boolean isDir;
    private final List<String> renamedFiles = new ArrayList<String>();
        
    MercurialHistoryParser(MercurialRepository repository) {
        this.repository = repository;
        mydir = repository.getDirectoryName() + File.separator;
    }

    /**
     * Parse the history for the specified file or directory. If a changeset is
     * specified, only return the history from the changeset right after the
     * specified one.
     *
     * @param file the file or directory to get history for
     * @param changeset the changeset right before the first one to fetch, or
     * {@code null} if all changesets should be fetched
     * @return history for the specified file or directory
     * @throws HistoryException if an error happens when parsing the history
     */
    History parse(File file, String changeset) throws HistoryException {
        isDir = file.isDirectory();
        try {
            Executor executor = repository.getHistoryLogExecutor(file, changeset);
            int status = executor.exec(true, this);

            if (status != 0) {
                throw new HistoryException("Failed to get history for: \"" +
                                           file.getAbsolutePath() +
                                           "\" Exit code: " + status);
            }
        } catch (IOException e) {
            throw new HistoryException("Failed to get history for: \"" +
                                       file.getAbsolutePath() + "\"", e);
        }

        // If a changeset to start from is specified, remove that changeset
        // from the list, since only the ones following it should be returned.
        // Also check that the specified changeset was found, otherwise throw
        // an exception.
        if (changeset != null) {
            repository.removeAndVerifyOldestChangeset(entries, changeset);
        }

        return new History(entries, renamedFiles);
    }

    /**
     * Process the output from the hg log command and insert the HistoryEntries
     * into the history field.
     *
     * @param input The output from the process
     * @throws java.io.IOException If an error occurs while reading the stream
     */
    @Override
    public void processStream(InputStream input) throws IOException {
        RuntimeEnvironment env = RuntimeEnvironment.getInstance();
        DateFormat df = repository.getDateFormat();
        BufferedReader in = new BufferedReader(new InputStreamReader(input));
        entries = new ArrayList<HistoryEntry>();
        String s;
        HistoryEntry entry = null;
        while ((s = in.readLine()) != null) {
            if (s.startsWith(MercurialRepository.CHANGESET)) {
                entry = new HistoryEntry();
                entries.add(entry);
                entry.setActive(true);
                entry.setRevision(s.substring(MercurialRepository.CHANGESET.length()).trim());
            } else if (s.startsWith(MercurialRepository.USER) && entry != null) {
                entry.setAuthor(s.substring(MercurialRepository.USER.length()).trim());
            } else if (s.startsWith(MercurialRepository.DATE) && entry != null) {
                Date date = new Date();
                try {
                    date = df.parse(s.substring(MercurialRepository.DATE.length()).trim());
                } catch (ParseException pe) {
                    OpenGrokLogger.getLogger().log(Level.WARNING,
                        "Could not parse date: " + s, pe);
                }
                entry.setDate(date);
            } else if (s.startsWith(MercurialRepository.FILES) && entry != null) {
                String[] strings = s.split(" ");
                for (int ii = 1; ii < strings.length; ++ii) {
                    if (strings[ii].length() > 0) {
                        File f = new File(mydir, strings[ii]);
                        try {
                            entry.addFile(env.getPathRelativeToSourceRoot(f, 0));
                        } catch (FileNotFoundException e) { // NOPMD
                            // If the file is not located under the source root,
                            // ignore it (bug #11664).
                        }
                    }
                }
            } else if (s.startsWith(MercurialRepository.FILE_COPIES) &&
                entry != null && isDir) {
                /* 
                 * 'file_copies:' should be present only for directories but
                 * we use isDir to be on the safe side.
                 */
                s = s.replaceFirst(MercurialRepository.FILE_COPIES, "");
                String[] splitArray = s.split("\\)");
                for (String part: splitArray) {
                     /*
                      * This will fail for file names containing ' ('.
                      */
                     String[] move = part.split(" \\(");
                     File f = new File(mydir + move[0]);
                     if (!move[0].isEmpty() && f.exists() &&
                         !renamedFiles.contains(move[0])) {
                             renamedFiles.add(move[0]);
                     }
                }
            } else if (s.startsWith(DESC_PREFIX) && entry != null) {
                entry.setMessage(decodeDescription(s));
            } else if (s.equals(MercurialRepository.END_OF_ENTRY)
                && entry != null) {
                    entry = null;
            } else if (s.length() > 0) {
                OpenGrokLogger.getLogger().log(Level.WARNING,
                    "Invalid/unexpected output {0} from hg log for repo {1}",
                    new Object[]{s, repository.getDirectoryName()});
            }
        }
    }

    /**
     * Decode a line with a description of a commit. The line is a sequence of
     * XML character entities that need to be converted to single characters.
     * This is to prevent problems if the log message contains one of the
     * prefixes that {@link #processStream(InputStream)} is looking for (bug
     * #405).
     *
     * This method is way too tolerant, and won't complain if the line has
     * a different format than expected. It will return weird results, though.
     *
     * @param line the XML encoded line
     * @return the decoded description
     */
    private String decodeDescription(String line) {
        StringBuilder out = new StringBuilder();
        int value = 0;

        // fetch the char values from the &#ddd; sequences
        for (int i = DESC_PREFIX.length(); i < line.length(); i++) {
            char ch = line.charAt(i);
            if (Character.isDigit(ch)) {
                value = value * 10 + Character.getNumericValue(ch);
            } else if (ch == ';') {
                out.append((char) value);
                value = 0;
            }
        }

        assert value == 0 : "description did not end with a semi-colon";

        return out.toString();
    }
}
