/**
 *#########################################################################
 *
 * A component of the Gatherer application, part of the Greenstone digital
 * library suite from the New Zealand Digital Library Project at the
 * University of Waikato, New Zealand.
 *
 * <BR><BR>
 *
 * Author: John Thompson, Greenstone Digital Library, University of Waikato
 *
 * <BR><BR>
 *
 * Copyright (C) 1999 New Zealand Digital Library Project
 *
 * <BR><BR>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * <BR><BR>
 *
 * This program 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 General Public License for more details.
 *
 * <BR><BR>
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 *########################################################################
 */
package org.greenstone.gatherer.shell;

import java.awt.Component;
import java.util.ArrayList;
import org.greenstone.gatherer.Configuration;
import org.greenstone.gatherer.DebugStream;
import org.greenstone.gatherer.Dictionary;
import org.greenstone.gatherer.Gatherer;
import org.greenstone.gatherer.cdm.CollectionDesignManager;
import org.greenstone.gatherer.gui.GProgressBar;
import org.greenstone.gatherer.util.StaticStrings;
/** This implementation of <i>GShellProgressMonitor</i> is designed to parse and translate the progress of a buildcol.pl call.
 * @author John Thompson, Greenstone Digital Library, University of Waikato
 * @version 2.3
 */
public class GBuildProgressMonitor
    implements GShellProgressMonitor {
    /** The minimum value = 0 */
    static final private int MIN = 0;
    /** In order to get a smoothish progress bar, set the maximum to a large number = 1000000 */
    static final private int MAX = 1000000;
    /** The various states the build progress state machine can be in, starting at the base state. */
    static final int BASE                          = -1;
    static final int BUILD                         =  0;
    static final int COMPRESSTEXT                  = 10;
    static final int COMPRESSTEXT_COLLECTTEXTSTATS = 11;
    static final int COMPRESSTEXT_CREATINGCOMPRESS = 12;
    static final int COMPRESSTEXT_COMPRESSINGTEXT  = 13;
    static final int INDEX                         = 20;
    static final int INDEX_CREATINGINDEXDIC        = 21;
    static final int INDEX_INVERTINGTEXT           = 22;
    static final int INDEX_CREATETHEWEIGHTS        = 23;
    static final int INDEX_CREATESTEMMEDDIC        = 24;
    static final int INDEX_CREATINGSTEMINDX        = 25;
    static final int CREATEINFODATA                = 30;
    static final int PHIND                         = 40;
    static final int PHIND_EXTRACTINGVOCAB         = 41;
    static final int PHIND_EXTRACTINGPHRASE        = 42;
    static final int PHIND_SORTANDRENUMBER         = 43;
    static final int PHIND_PHRASEDATABASES         = 44;
    static final int PHIND_WORDLEVELINDEXES        = 45;
    static final int PHIND_DOCINFODATABASES        = 46;
    static final int CREATINGAUXILARY              = 50;
    /** Now the language independant sentinel strings. */
    static final String BADARGUMENT_ELEMENT    = "BadArgument";
    static final String BADARGUMENTVALUE_ELEMENT = "BadArgumentValue";
    static final String BADCLASSIFIER_ELEMENT = "BadClassifier";
    static final String BADPLUGIN_ELEMENT = "BadPlugin";
    static final String BUILD_ELEMENT          = "Build";
    static final String COLLECTTEXTSTATS_VALUE = "CollectTextStats";
    static final String COMPRESSINGTEXT_VALUE  = "CompressingText";
    static final String COMPRESSTEXT_VALUE     = "CompressText";
    static final String CREATINGAUXILARY_VALUE = "CreatingAuxilary";
    static final String CREATINGCOMPRESS_VALUE = "CreatingCompress";
    static final String CREATINGINDEXDIC_VALUE = "CreatingIndexDic";
    static final String CREATINGSTEMINDX_VALUE = "CreatingStemIndx";
    static final String CREATEINFODATA_VALUE   = "CreateInfoData";
    static final String CREATESTEMMEDDIC_VALUE = "CreateStemmedDic";
    static final String CREATETHEWEIGHTS_VALUE = "CreateTheWeights";
    static final String DOCINFODATABASES_VALUE = "DocInfoDatabases";
    static final String EXTRACTINGPHRASE_VALUE = "ExtractingPhrase";
    static final String EXTRACTINGVOCAB_VALUE  = "ExtractingVocab";
    static final String FATALERROR_ELEMENT     = "FatalError";
    static final String INDEX_VALUE            = "Index";
    static final String INVERTINGTEXT_VALUE    = "InvertingText";
    static final String NAME_ATTRIBUTE         = "name";
    static final String PHASE_ELEMENT          = "Phase";
    static final String PHIND_VALUE            = "Phind";
    static final String PHRASEDATABASES_VALUE  = "PhraseDatabases";
    static final String SKIPCREATINGCOMP_VALUE = "SkipCreatingComp";
    static final String SORTANDRENUMBER_VALUE  = "SortAndRenumber";
    static final String SOURCE_ATTRIBUTE       = "source";
    static final String STAGE_ELEMENT          = "Stage";
    static final String WARNING_ELEMENT        = "Warning";   
    static final String WORDLEVELINDEXES_VALUE = "WordLevelIndexes";

    static final private String ARGUMENT_ATTRIBUTE = "a";
    static final private String CLASSIFIER_ATTRIBUTE = "c";
    static final private String PLUGIN_ATTRIBUTE = "p";

    /** Indicates if the GUI has asked the process this object monitors to stop. */
    private boolean stop = false;
    /** The current number of stages we have completed. */
    private int current_stages = 0;
    /** The number of stages we are expecting to complete. */
    private int expected_stages = 0;

    private int threshold = Configuration.SYSTEMS_MODE;
    /** The current state we are in. The state will change depending on the next flag recieved. */
    private int state = BASE;
    /** The progress bar this monitor updates. */
    private GProgressBar progress_bar;
    /** A progress bar that is shared between this this listener and the Import monitor. */
    private GProgressBar shared_progress_bar;

    /** Construct a new GBuildProgressMonitor. */
    public GBuildProgressMonitor(GProgressBar shared_progress_bar) {
	this.shared_progress_bar = shared_progress_bar;
	progress_bar = new GProgressBar();
	progress_bar.setMaximum(MAX);
	progress_bar.setMinimum(MIN);
	progress_bar.setString(null);
	progress_bar.setStringPainted(true);
	setValue(MIN);
    }

    /** Method to register a new progress bar with this monitor.
     * @param progress_bar The new <strong>GProgressBar</strong>.
     */	 
    public void addProgressBar(GProgressBar progress_bar) {
	this.progress_bar = progress_bar;
	progress_bar.setMaximum(MAX);
	progress_bar.setMinimum(MIN);
	progress_bar.setStringPainted(false);
	progress_bar.setString(null);
	setValue(MIN);
    }

    /** Determine the script exit value according to the progress monitor. This gets around a problem where several script failures actually result with a successful exit value.
     * @return A <i>int</i> with a value of zero if and only if the script was successful.
     */
    public int exitValue() {
	if(state == BASE) {
	    return 0;
	}
	return 1;
    }
    /** Method to retrieve whatever control is being used as the progress indicator. Usually a <strong>GProgressBar</strong> but there may be others implemented later.
     * @return A <strong>Component</strong> on which the progress of the process is being displayed.
     */
    public Component getProgress() {
	return progress_bar;
    }

    public GProgressBar getSharedProgress() {
	return shared_progress_bar;
    }

    public void messageOnProgressBar(String message)
    {
	if (message!=null && !message.equals("")) {
	    progress_bar.setString(message);
	    shared_progress_bar.setString(message);
	}
	else {
	    progress_bar.setString(null);
	    shared_progress_bar.setString(null);
	}
    }

    /** Method to determine the state of the stop flag, which may be set by the visual component that created this monitor.
     * @return A <strong>boolean</strong> indicating if the process has been asked to stop.
     */
    public synchronized boolean hasSignalledStop() {
	return stop;
    }
    /** Inform the progress bar that it should programatically increment progress by one step. */
    public void increment() {
	// There is no reason this should be called for a build.
    }

    /** This method is used to 'feed in' a line of text captured from the process.
     * @param queue a queue which at the moment should contain a single GShellEvent
     */
    public void process(ArrayList queue) {
	GShellEvent event = (GShellEvent) queue.get(0);
	// Remove 'build.pl> ' bit
	String line = event.getMessage();
	line = line.substring(line.indexOf(StaticStrings.GREATER_THAN_CHARACTER) + 1);
	line = line.trim();
	// We are only really interested in processing pseudo-XML tags
	if(line.startsWith(StaticStrings.LESS_THAN_CHARACTER) && line.endsWith(StaticStrings.GREATER_THAN_CHARACTER)) {
	    // All events are vetoed
	    event.veto();
	    
	    // Create a new element from it
	    GShellElement element = new GShellElement(line);
	    // Now change states within the state machine as appropriate. A state change may subsequently incur a progress update.
	    String name = null;
	    String value = null;
	    switch(state) {
	    case BASE:
		// The only thing we would expect is a build message
		name = element.getElementName();
		if(name.equals(BUILD_ELEMENT)) {
		    progress_bar.setIndeterminate(false);
		    state = BUILD;
		    // Produce a lower detail message if required
		    if(Configuration.getMode() <= threshold && !stop) {
			queue.add(new GShellEvent(event.getSource(), 0, event.getType(), Dictionary.get("GShell.Build.BuildBegun1"), event.getStatus()));
		    }
		}
		else {
		    // Unknown command, go indeterminate
		    DebugStream.println("Unknown name: " + name);
		    progress_bar.setIndeterminate(true);
		}
		name = null;
		break;
	    case BUILD:
		// The only thing we would expect is a stage element or a 
		// bad classifier arg name
		name = element.getElementName();

		if(name.equals(STAGE_ELEMENT)) {
		    progress_bar.setIndeterminate(false);
		    // Now we determine what stage we are moving to
		    value = element.getAttribute(NAME_ATTRIBUTE);
		    if(value.equals(COMPRESSTEXT_VALUE)) {
			state = COMPRESSTEXT;
			if(Configuration.getMode() <= threshold) {
			    queue.add(new GShellEvent(event.getSource(), 0, event.getType(), Dictionary.get("GShell.Build.CompressText"), event.getStatus()));
			}
		    }
		    else if(value.equals(INDEX_VALUE)) {
			state = INDEX;
			if(Configuration.getMode() <= threshold) {
			    String args[] = new String[1];
			    args[0] = element.getAttribute(SOURCE_ATTRIBUTE);
			    queue.add(new GShellEvent(event.getSource(), 0, event.getType(), Dictionary.get("GShell.Build.Index", args), event.getStatus()));
			    args = null;
			}
		    }
		    else if(value.equals(CREATEINFODATA_VALUE)) {
			state = CREATEINFODATA;
			if(Configuration.getMode() <= threshold) {
			    queue.add(new GShellEvent(event.getSource(), 0, event.getType(), Dictionary.get("GShell.Build.InfoDatabase"), event.getStatus()));
			}
		    }
		    else if(value.equals(PHIND_VALUE)) {
			state = PHIND;
			if(Configuration.getMode() <= threshold) {
			    queue.add(new GShellEvent(event.getSource(), 0, event.getType(), Dictionary.get("GShell.Build.Phind"), event.getStatus()));
			}
		    }
		    else if(value.equals(CREATINGAUXILARY_VALUE)) {
			state = CREATINGAUXILARY;
			if(Configuration.getMode() <= threshold) {
			    queue.add(new GShellEvent(event.getSource(), 0, event.getType(), Dictionary.get("GShell.Build.Auxilary"), event.getStatus()));
			}
		    }
		    else {
			// Unknown command, go indeterminate
			DebugStream.println("Unknown value: " + value);
			progress_bar.setIndeterminate(true);
		    }
		    value = null;
		}
		// The build end message indicates a successfulish build.
		else if(name.equals(BUILD_ELEMENT) && element.isClosed()) {
		    progress_bar.setIndeterminate(false);
		    setValue(MAX);
		    state = BASE;
		    if(Configuration.getMode() <= threshold) {
			queue.add(new GShellEvent(event.getSource(), 0, event.getType(), Dictionary.get("GShell.Build.BuildComplete1"), event.getStatus()));
		    }
		}
		// We had a bad argument to a plugin/classifier
		else if (name.equals(BADARGUMENT_ELEMENT)) {
		    if(Configuration.getMode() <= threshold) {
			String args[] = new String[1];
			args[0] = element.getAttribute(ARGUMENT_ATTRIBUTE);    
			queue.add(new GShellEvent(event.getSource(), 0, event.getType(), Dictionary.get("GShell.BadArgument", args), event.getStatus()));
			args = null;
		    }
		}
		// We had a bad argument value to a plugin/clasifier
		else if (name.equals(BADARGUMENTVALUE_ELEMENT)) {
		    if(Configuration.getMode() <= threshold) {
			String args[] = new String[1];
			args[0] = element.getAttribute(ARGUMENT_ATTRIBUTE);    
			queue.add(new GShellEvent(event.getSource(), 0, event.getType(), Dictionary.get("GShell.BadArgumentValue", args), event.getStatus()));
			args = null;
		    }
		}
		// And this one tellsa us the plugin
		else if (name.equals(BADPLUGIN_ELEMENT)) {
		    if(Configuration.getMode() <= threshold) {
			String args[] = new String[1];
			args[0] = element.getAttribute(PLUGIN_ATTRIBUTE);    
			queue.add(new GShellEvent(event.getSource(), 0, event.getType(), Dictionary.get("GShell.BadPluginOptions", args), event.getStatus()));
			args = null;
		    }
		}
		// or the classifier
		else if (name.equals(BADCLASSIFIER_ELEMENT)) {
		    if(Configuration.getMode() <= threshold) {
			String args[] = new String[1];
			args[0] = element.getAttribute(CLASSIFIER_ATTRIBUTE);    
			queue.add(new GShellEvent(event.getSource(), 0, event.getType(), Dictionary.get("GShell.BadClassifierOptions", args), event.getStatus()));
			args = null;
		    }
		}
		
		else {
		    // Unknown command, go indeterminate
		    DebugStream.println("Unknown name: " + name);
		    progress_bar.setIndeterminate(true);
		}
		name = null;
		break;
	    case COMPRESSTEXT:
		// We are either dealing with a phase, aa fatal error or an end stage
		name = element.getElementName();
		if(name.equals(PHASE_ELEMENT)) {
		    // There are three phases within compressing text (or of four possible ones)
		    value = element.getAttribute(NAME_ATTRIBUTE);
		    if(value.equals(COLLECTTEXTSTATS_VALUE)) {
			// Nothing to do
		    }
		    else if(value.equals(CREATINGCOMPRESS_VALUE) || value.equals(SKIPCREATINGCOMP_VALUE)) {
			// One third complete
			progress_bar.setIndeterminate(false);
			setValue((int)(((double)current_stages + 0.3d) * MAX) / expected_stages);
		    }
		    else if(value.equals(COMPRESSINGTEXT_VALUE)) {
			// Two thirds complete
			progress_bar.setIndeterminate(false);
		        setValue((int)(((double)current_stages + 0.6d) * MAX) / expected_stages);
		    }
		    else {
			// Unknown command, go indeterminate
			DebugStream.println("Unknown value: " + value);
			progress_bar.setIndeterminate(true);
		    }
		    value = null;
		}
		// Note that a fatal error is always followed by an end-stage, but not an end build. Thus the final state will be build (not base) and calls to exitValue() with indicate failure.
		else if(name.equals(FATALERROR_ELEMENT)) {
		    // On simplier modes produce a meaningful message
		    queue.add(new GShellEvent(event.getSource(), 0, event.getType(), Dictionary.get("GShell.Build.FatalError"), event.getStatus()));
		}
		else if(name.equals(STAGE_ELEMENT) && element.isClosed()) {
		    // Increase progress so that stage is complete.
		    current_stages++;
		    progress_bar.setIndeterminate(false);
		    setValue((current_stages * MAX) / expected_stages);
		    state = BUILD;
		}
		else {
		    // Unknown command, go indeterminate
		    DebugStream.println("Unknown name: " + name);
		    progress_bar.setIndeterminate(true);
		}
		name = null;
		break;
	    case INDEX:
		/** @todo - If on lower modes output a meaningful message. */
		name = element.getElementName();
		if(name.equals(PHASE_ELEMENT)) {
		    value = element.getAttribute(NAME_ATTRIBUTE);
		    if(value.equals(CREATINGINDEXDIC_VALUE)) {
			// Nothing to do
		    }
		    else if(value.equals(INVERTINGTEXT_VALUE)) {
			// One fifth complete
			progress_bar.setIndeterminate(false);
			setValue((int)(((double)current_stages + 0.2d) * MAX) / expected_stages);
		    }
		    else if(value.equals(CREATETHEWEIGHTS_VALUE)) {
			// Two fifths complete
			progress_bar.setIndeterminate(false);
			setValue((int)(((double)current_stages + 0.4d) * MAX) / expected_stages);
		    }
		    else if(value.equals(CREATESTEMMEDDIC_VALUE)) {
			// Three fifths complete
			progress_bar.setIndeterminate(false);
			setValue((int)(((double)current_stages + 0.6d) * MAX) / expected_stages);
		    }
		    else if(value.equals(CREATINGSTEMINDX_VALUE)) {
			// Four fifths complete
			progress_bar.setIndeterminate(false);
			setValue((int)(((double)current_stages + 0.8d) * MAX) / expected_stages);
		    }
		    else {
			// Unknown command, go indeterminate
			DebugStream.println("Unknown value: " + value);
			progress_bar.setIndeterminate(true); 
		    }
		    value = null;
		}
		else if(name.equals(WARNING_ELEMENT)) {
		    /** @todo - If on lower modes output a meaningful message. */
		}
		// Note that a fatal error is always followed by an end-stage, but not an end build. Thus the final state will be build (not base) and calls to exitValue() with indicate failure.
		else if(name.equals(FATALERROR_ELEMENT)) {
		    // On lower modes output a meaningful message.
		    
		}
		else if(name.equals(STAGE_ELEMENT) && element.isClosed()) {
		    // Increase progress so that stage is complete.
		    current_stages++;
		    progress_bar.setIndeterminate(false);
		    setValue((current_stages * MAX) / expected_stages);
		    state = BUILD;
		}
		else {
		    // Unknown command, go indeterminate
		    DebugStream.println("Unknown name: " + name);
		    progress_bar.setIndeterminate(true); 
		}
		name = null;
		break;
	    case PHIND:
		name = element.getElementName();
		if(name.equals(PHASE_ELEMENT)) {
		    value = element.getAttribute(NAME_ATTRIBUTE);
		    if(value.equals(EXTRACTINGVOCAB_VALUE)) {
			// Nothing to do
		    }
		    else if(value.equals(EXTRACTINGPHRASE_VALUE)) {
			// One sixth complete
			progress_bar.setIndeterminate(false);
			setValue((int)(((double)current_stages + 0.16d) * MAX) / expected_stages);
		    }
		    else if(value.equals(SORTANDRENUMBER_VALUE)) {
			// Two sixths complete
			progress_bar.setIndeterminate(false);
		        setValue((int)(((double)current_stages + 0.33d) * MAX) / expected_stages);
		    }
		    else if(value.equals(PHRASEDATABASES_VALUE)) {
			// Three sixths complete
			progress_bar.setIndeterminate(false);
		        setValue((int)(((double)current_stages + 0.5d) * MAX) / expected_stages);
		    }
		    else if(value.equals(WORDLEVELINDEXES_VALUE)) {
			// Four sixths complete
			progress_bar.setIndeterminate(false);
			setValue((int)(((double)current_stages + 0.66d) * MAX) / expected_stages);
		    }
		    else if(value.equals(DOCINFODATABASES_VALUE)) {
			// Five sixths complete
			progress_bar.setIndeterminate(false);
			setValue((int)(((double)current_stages + 0.83d) * MAX) / expected_stages);
		    }
		    else {
			// Unknown command, go indeterminate
			DebugStream.println("Unknown value: " + value);
			progress_bar.setIndeterminate(true); 
		    }
		    value = null;
		}
		else if(name.equals(STAGE_ELEMENT) && element.isClosed()) {
		    // Increase progress so that stage is complete.
		    current_stages++;
		    progress_bar.setIndeterminate(false);
		    setValue((current_stages * MAX) / expected_stages);
		    state = BUILD;
		}
		else {
		    // Unknown command, go indeterminate
		    DebugStream.println("Unknown name: " + name);
		    progress_bar.setIndeterminate(true); 
		}
		name = null;
		break;
	    case CREATEINFODATA:
	    case CREATINGAUXILARY:
		name = element.getElementName();
		if(name.equals(STAGE_ELEMENT) && element.isClosed()) {
		    // Another stage complete
		    current_stages++;
		    progress_bar.setIndeterminate(false);
		    setValue((current_stages * MAX) / expected_stages);
		    state = BUILD;
		}
		else {
		    // Unknown command, go indeterminate
		    DebugStream.println("Unknown name: " + name);
		    progress_bar.setIndeterminate(true);
		}
		name = null;
		break;
	    default:
		// Unknown command, go indeterminate
		DebugStream.println("Unknown name: " + name);
		progress_bar.setIndeterminate(true);
	    }
	}
	// If we are dealing with lower detail modes then veto any other messages. We do this here as the progress monitors are garuenteed to recieve this message before anything else
	else if(Configuration.getMode() <= threshold) {
	    event.veto();
	}
    }

    public void reset() {
	current_stages = 0;
	expected_stages = 0;
	progress_bar.setIndeterminate(false);
	progress_bar.setString(null);
	setValue(MIN);
	state = BASE;
	progress_bar.updateUI();
    }

    public void saving() {
    }

    public void setSharedProgressBar(GProgressBar shared_progress_bar) {
	this.shared_progress_bar = shared_progress_bar;
    }

    /** Since the creator of this process monitor is actually in the GUI, this class provides the simpliest way to send a cancel process message between the two.
     * @param state The desired state of the stop flag as a <strong>boolean</strong>.
     */
    public synchronized void setStop(boolean state) {
	this.stop = state;
	progress_bar.setIndeterminate(false);
    }
    /** This method resets this monitor to the start, reseting the process parsing and progress bar.
     * TODO Everthing.
     */
    public void start() {
	stop = false;
	setValue(MIN);
	expected_stages = 3; // Always compress text, create info database and auxiliary creation
	// Retrieve cdm.
	CollectionDesignManager cdm = Gatherer.c_man.getCollection().cdm;
	// If we are using MGPP the number of indexes is always 1
	if(cdm.index_manager.isMGPP()) {
	    expected_stages = expected_stages + 1;
	} else if (cdm.index_manager.isLucene()) {
	    // lucene no of indexes == number of levels
	    int num_levels = cdm.index_manager.getNumLevels();
	    if (num_levels == 0) num_levels = 1;
	    expected_stages = expected_stages + num_levels;
	}
	else {
	    int num_indexes = cdm.index_manager.getNumIndexes();
	    int num_partitions = cdm.subcollectionindex_manager.getSize();
	    if(num_partitions > 0) {
		num_indexes = num_indexes * num_partitions;
	    }
	    int num_languages = cdm.language_manager.getSize();
	    if(num_languages > 0) {
		num_indexes = num_indexes * num_languages;
	    }
	    expected_stages = expected_stages + num_indexes;
	}
	// And if we have a phind classifier, we have one further index
	if(cdm.classifier_manager.isPhindClassifierAssigned()) {
	    expected_stages = expected_stages + 1;
	}
	cdm = null;
    }
    /** This method indicates the process is complete. 
     * TODO Everthing.
     */
    public void stop() {
	progress_bar.setIndeterminate(false);
	setValue(MAX);
    }

    private void setValue(int value) {
	progress_bar.setValue(value);
	// Incrementing the shared progress bar is a little more problematic as we may very well be trying to set it to MIN but instead set it to halfway if we're not careful.
	if(value > MIN) {
	    shared_progress_bar.setValue(MAX + value);
	}
    }
}

