/* Author: Jeff Dalton <J.Dalton@ed.ac.uk>
 * Updated: Tue Dec  4 23:38:51 2001 by Jeff Dalton
 * Copyright: (c) 2001, AIAI, University of Edinburgh
 */

package ix.ideel;

import ix.ideel.event.*;
import ix.icore.process.PNode;
import ix.icore.process.PNodeMaker;
import ix.icore.process.StatusValues;
import ix.icore.domain.Schema;
import ix.icore.IXAgent;
import ix.icore.Issue;
import ix.icore.BasicIssue;
import ix.icore.Report;
import ix.iface.util.Reporting;
import ix.util.*;
import ix.util.lisp.*;
import ix.util.match.MatchEnv;

import java.util.*;

/**
 * An issue managed by I-DEEL.  These issues double as the actions in
 * a process being carried out the I-DEEL and its user.  Issues can
 * be broken down into subissues and can be constained to be handled
 * before or after other issues.  Each issue has a set of "options"
 * that corresponed to the different ways in which it might be
 * handled.
 */

public class IdeelIssue extends PNode implements Issue, StatusValues {

    Object verb;
    LList parameters;
    // LList pattern;

    // int status = STATUS_BLANK;
    
    int priority = PRIORITY_NONE;

    Map properties = new HashMap(7);

    String shortDescription;
    String fullDescription;

    String comments = "";

    public String reportRef = null; // identifies reports related to this issue

    protected LListCollector options = new LListCollector();

    protected LListCollector reports = new LListCollector();

    protected LListCollector listeners = new LListCollector();

    public IdeelIssue(String text) {
	this(null, Lisp.elementsFromString(text));
    }

    public IdeelIssue(LList pattern) {
	this(null, pattern);
    }

    IdeelIssue(IdeelIssue parent, LList pattern) {
	super(parent, pattern);
	pattern = this.pattern;	// in case super changed it to put in Variables
	Debug.assert(!pattern.isNull(), "empty schema pattern");
	// Need to do computeStatus() here rather than in superclass,
	// so our setStatus method can refer to our fields such as
	// properties.  /\/
	computeStatus();
	this.verb = pattern.car();
	this.parameters = pattern.cdr();
	this.fullDescription = Lisp.elementsToString(pattern);
	this.shortDescription = fullDescription; // for now /\/
	if (parent != null)
	    this.priority = parent.priority;
    }

    public IdeelIssue(Issue i) {
	this(null, Lisp.cons(i.getVerb(), (LList)i.getParameters()));
	setPriority(i.getPriority());
	properties.putAll((Map)i.getProperties());
    }

    public Object getVerb() {
	return verb;
    }

    public Object getParameters() {
	return parameters;
    }

    public int getStatus() {
	return status;
    }

    public void setStatus(int status) {
	if (this.status != status) {
	    super.setStatus(status);
	    if (getReportBack())
		handleReportBack();
	    fireStatusChanged();
	}
    }

    public int getPriority() {
	return priority;
    }

    public void setPriority(int priority) {
	this.priority = priority;
	firePriorityChanged();
    }

    public Map getProperties() {
	return properties;
    }

    public void setProperties(Map properties) {
	this.properties = properties;
    }

    public String getSenderId() {
	return (String)properties.get("sender-id");
    }

    public void setSenderId(String id) {
	 properties.put("sender-id", id);
    }

    public String getRef() {
	return (String)properties.get("ref");
    }

    public void setRef(String ref) {
	properties.put("ref", ref);
    }

    public boolean getReportBack() {
	String value = (String)properties.get("report-back");
	return value != null && value.equals("yes");
    }

    public void setReportBack(boolean value) {
	properties.put("report-back", value ? "yes" : "no");
    }

    public LList getOptions() {
	return options.contents();
    }

    /**
     * Add an option to this issue.  Note that an option with the same
     * description may already exist.  We regard that as a problem for
     * the issueListeners.  For example, a user interface might use
     * the description as a way to identify the option and may not want
     * any ambiguity.
     */
    public void addOption(IssueOption opt) {
	Debug.assert(!hasOption(opt), "Option added twice", opt);
	IssueOption exists = findOption(opt.shortDescription);
	if (exists != null) {
	    Debug.noteln("Option description not unique",
			 Util.quote(shortDescription));
	}
	options.addElement(opt);
	fireNewOption(opt);
    }

    boolean hasOption(IssueOption opt) {
	return options.contents().find(opt);
    }

    IssueOption findOption(String shortDescription) {
	for (Enumeration e = options.elements(); e.hasMoreElements();) {
	    IssueOption opt = (IssueOption)e.nextElement();
	    if (opt.shortDescription.equals(shortDescription))
		return opt;
	}
	return null;
    }

    boolean actionCanBeTakenNow(IssueOption opt) {
	// Allow any option when status is POSSIBLE and no option when
	// the status is something else.
	// /\/: Not clear what we should do when the usuer manually
	// specifies an expansion.  Right now, this method isn't called.
	return status == STATUS_POSSIBLE;
    }

    boolean wantsReport(Report report) {
	String ref = (String)report.getProperties().get("ref");
	return ref != null && ref.equals(reportRef);
    }

    void addReport(Report report) {
	Debug.assert(wantsReport(report), "unwanted report");
	Debug.noteln("Issue " + shortDescription + " accepts " + report);

	String lineSeparator = System.getProperty("line.separator");
	comments = comments
	    + (comments.equals("") || comments.endsWith(lineSeparator)
	       ? "" : lineSeparator)
	    + Reporting.reportDescription(report) + lineSeparator;

	reports.addElement(report);
	fireNewReport(report);

	setStatusBasedOn(report);

    }

    void setStatusBasedOn(Report report) {
	// /\/: Change status even though officially we may not
	// be able to make this transition (e.g. from COMPLETE
	// to EXECUTING).
	if (!report.isCompletion())
	    setStatus(STATUS_EXECUTING);
	else if (report.isSuccess())
	    setStatus(STATUS_COMPLETE);
	else
	    setStatus(STATUS_IMPOSSIBLE);
    }

    List getReports() {
	return reports.contents(); // the contents are less modifiable
    }

    void handleReportBack() {
	Debug.assert(getReportBack());

	// See what we need to say
	String type;
	if (status == STATUS_COMPLETE)
	    type = "success";
	else if (status == STATUS_IMPOSSIBLE)
	    type = "failure";
	else return;

	// Construct the Report.
	Report report = new Report("done - " + type);
	report.setReportType(type);
	report.setSenderId((String)IXAgent.getAgent().getAgentIPCName());
	report.setRef(getRef());

	// Send.
	IPC.sendObject(getSenderId(), // whoever sent this issue to us
		       report);
    }


    /**
     * Sends a copy of this issue to another agent, asking the destination
     * agent to send reports back.
     */
    public void forwardIssue(Object destination) {
	forwardIssue(destination, true);
    }

    /**
     * Sends a copy of this issue to another agent, optionally asking
     * the destination agent to send reports back.
     */
    public void forwardIssue(Object destination, boolean reportBack) {
	Object ipcName = IXAgent.getAgent().getAgentIPCName();
	Issue icopy = new BasicIssue(this);
	ensureReportRef();
	icopy.setSenderId((String)ipcName);
	icopy.setReportBack(reportBack);
	icopy.setRef(reportRef);
	IPC.sendObject(destination, icopy);
    }

    /**
     * Generates a report-ref for this issue if it does not already
     * have one.
     */
    public String ensureReportRef() {
	Object ipcName = IXAgent.getAgent().getAgentIPCName();
	if (reportRef == null) {
	    reportRef = Util.generateName(ipcName.toString());
	    Debug.noteln("Set reportRef of " + this
			 + " to " + Util.quote(reportRef));
	}
	return reportRef;
    }

    void addIssueListener(IssueListener listener) {
	// /\/: What is the Java convention on adding if already there?
	if (!listeners.contains(listener))
	    listeners.addElement(listener);
    }

    void fireStatusChanged() {
	IssueEvent event = new IssueEvent(this);
	// This method may be called before listeners has been
	// initialized because PNode init calls computeStatus.
	if (listeners != null) {
	    for (Enumeration e = listeners.elements(); e.hasMoreElements();) {
		IssueListener listener = (IssueListener)e.nextElement();
		listener.statusChanged(event);
	    }
	}
    }

    void firePriorityChanged() {
	IssueEvent event = new IssueEvent(this);
	for (Enumeration e = listeners.elements(); e.hasMoreElements();) {
	    IssueListener listener = (IssueListener)e.nextElement();
	    listener.priorityChanged(event);
	}
    }

    void fireNewOption(IssueOption opt) {
	IssueEvent event = new IssueEvent(this);
	for (Enumeration e = listeners.elements(); e.hasMoreElements();) {
	    IssueListener listener = (IssueListener)e.nextElement();
	    listener.newOption(event, opt);
	}
    }

    void fireNewReport(Report report) {
	IssueEvent event = new IssueEvent(this);
	for (Enumeration e = listeners.elements(); e.hasMoreElements();) {
	    IssueListener listener = (IssueListener)e.nextElement();
	    listener.newReport(event, report);
	}
    }

    void fireIssueEdited() {
	IssueEvent event = new IssueEvent(this);
	for (Enumeration e = listeners.elements(); e.hasMoreElements();) {
	    IssueListener listener = (IssueListener)e.nextElement();
	    listener.issueEdited(event);
	}
    }

    void expandOneLevel(Schema schema, MatchEnv env) {
	expandOneLevel(schema, env, new PNodeMaker() {
	    public PNode makePNode(PNode parent, LList pattern) {
		return new IdeelIssue((IdeelIssue)parent, pattern);
	    }
	});
    }

}
