/*
 * Copyright (C) 2009 JavaRosa
 *
 * 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 org.odk.collect.android.logic;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Vector;

import org.javarosa.core.model.FormDef;
import org.javarosa.core.model.FormIndex;
import org.javarosa.core.model.GroupDef;
import org.javarosa.core.model.IDataReference;
import org.javarosa.core.model.IFormElement;
import org.javarosa.core.model.SubmissionProfile;
import org.javarosa.core.model.condition.EvaluationContext;
import org.javarosa.core.model.data.IAnswerData;
import org.javarosa.core.model.data.StringData;
import org.javarosa.core.model.instance.FormInstance;
import org.javarosa.core.model.instance.TreeElement;
import org.javarosa.core.services.IPropertyManager;
import org.javarosa.core.services.PrototypeManager;
import org.javarosa.core.services.transport.payload.ByteArrayPayload;
import org.javarosa.form.api.FormEntryCaption;
import org.javarosa.form.api.FormEntryController;
import org.javarosa.form.api.FormEntryModel;
import org.javarosa.form.api.FormEntryPrompt;
import org.javarosa.model.xform.XFormSerializingVisitor;
import org.javarosa.model.xform.XFormsModule;
import org.javarosa.model.xform.XPathReference;
import org.javarosa.xform.parse.XFormParser;
import org.javarosa.xpath.XPathParseTool;
import org.javarosa.xpath.expr.XPathExpression;
import org.odk.collect.android.exception.JavaRosaException;
import org.odk.collect.android.views.ODKView;

import android.util.Log;

/**
 * This class is a wrapper for Javarosa's FormEntryController. In theory, if you wanted to replace
 * javarosa as the form engine, you should only need to replace the methods in this file. Also, we
 * haven't wrapped every method provided by FormEntryController, only the ones we've needed so far.
 * Feel free to add more as necessary.
 *
 * @author carlhartung
 */
public class FormController {

    private static final String t = "FormController";

    public static final boolean STEP_INTO_GROUP = true;
    public static final boolean STEP_OVER_GROUP = false;

    /**
     * OpenRosa metadata tag names.
     */
    private static final String INSTANCE_ID = "instanceID";
    private static final String INSTANCE_NAME = "instanceName";

    /**
     * OpenRosa metadata of a form instance.
     *
     * Contains the values for the required metadata
     * fields and nothing else.
     *
     * @author mitchellsundt@gmail.com
     *
     */
    public static final class InstanceMetadata {
        public final String instanceId;
        public final String instanceName;

        InstanceMetadata( String instanceId, String instanceName ) {
            this.instanceId = instanceId;
            this.instanceName = instanceName;
        }
    };
    
    /**
     * Classes needed to serialize objects. Need to put anything from JR in here.
     */
    private final static String[] SERIALIABLE_CLASSES = {
    		"org.javarosa.core.services.locale.ResourceFileDataSource", // JavaRosaCoreModule
    		"org.javarosa.core.services.locale.TableLocaleSource", // JavaRosaCoreModule
            "org.javarosa.core.model.FormDef",
			"org.javarosa.core.model.SubmissionProfile", // CoreModelModule
			"org.javarosa.core.model.QuestionDef", // CoreModelModule
			"org.javarosa.core.model.GroupDef", // CoreModelModule
			"org.javarosa.core.model.instance.FormInstance", // CoreModelModule
			"org.javarosa.core.model.data.BooleanData", // CoreModelModule
			"org.javarosa.core.model.data.DateData", // CoreModelModule
			"org.javarosa.core.model.data.DateTimeData", // CoreModelModule
			"org.javarosa.core.model.data.DecimalData", // CoreModelModule
			"org.javarosa.core.model.data.GeoPointData", // CoreModelModule
			"org.javarosa.core.model.data.GeoShapeData", // CoreModelModule
			"org.javarosa.core.model.data.GeoTraceData", // CoreModelModule
			"org.javarosa.core.model.data.IntegerData", // CoreModelModule
			"org.javarosa.core.model.data.LongData", // CoreModelModule
			"org.javarosa.core.model.data.MultiPointerAnswerData", // CoreModelModule
			"org.javarosa.core.model.data.PointerAnswerData", // CoreModelModule
			"org.javarosa.core.model.data.SelectMultiData", // CoreModelModule
			"org.javarosa.core.model.data.SelectOneData", // CoreModelModule
			"org.javarosa.core.model.data.StringData", // CoreModelModule
			"org.javarosa.core.model.data.TimeData", // CoreModelModule
			"org.javarosa.core.model.data.UncastData", // CoreModelModule
			"org.javarosa.core.model.data.helper.BasicDataPointer", // CoreModelModule
			"org.javarosa.core.model.Action", // CoreModelModule
			"org.javarosa.core.model.actions.SetValueAction" // CoreModelModule
    };

    private static boolean isJavaRosaInitialized = false;
    
    /**
     * Isolate the initialization of JavaRosa into one method, called first 
     * by the Collect Application.  Called subsequently whenever the Preferences
     * dialogs are exited (to potentially update username and email fields).
     * 
     * @param mgr
     */
    public static synchronized void initializeJavaRosa(IPropertyManager mgr) {
		if ( !isJavaRosaInitialized ) {
            // need a list of classes that formdef uses
            // unfortunately, the JR registerModule() functions do more than this.
            // register just the classes that would have been registered by:
            // new JavaRosaCoreModule().registerModule();
            // new CoreModelModule().registerModule();
            // replace with direct call to PrototypeManager
            PrototypeManager.registerPrototypes(SERIALIABLE_CLASSES);
            new XFormsModule().registerModule();

            isJavaRosaInitialized = true;
		}
        
		// needed to override rms property manager
		org.javarosa.core.services.PropertyManager
				.setPropertyManager(mgr);
    }

    private File mMediaFolder;
    private File mInstancePath;
    private FormEntryController mFormEntryController;
    private FormIndex mIndexWaitingForData = null;

    public FormController(File mediaFolder, FormEntryController fec, File instancePath) {
    	mMediaFolder = mediaFolder;
        mFormEntryController = fec;
        mInstancePath = instancePath;
    }

    public FormDef getFormDef() {
        return mFormEntryController.getModel().getForm();
    }

    public File getMediaFolder() {
    	return mMediaFolder;
    }

    public File getInstancePath() {
    	return mInstancePath;
    }

    public void setInstancePath(File instancePath) {
    	mInstancePath = instancePath;
    }

    public void setIndexWaitingForData(FormIndex index) {
    	mIndexWaitingForData = index;
    }

    public FormIndex getIndexWaitingForData() {
    	return mIndexWaitingForData;
    }

    /**
     * For logging purposes...
     *
     * @param index
     * @return xpath value for this index
     */
    public String getXPath(FormIndex index) {
    	String value;
    	switch ( getEvent() ) {
    	case FormEntryController.EVENT_BEGINNING_OF_FORM:
    		value = "beginningOfForm";
    		break;
    	case FormEntryController.EVENT_END_OF_FORM:
    		value = "endOfForm";
    		break;
    	case FormEntryController.EVENT_GROUP:
    		value = "group." + index.getReference().toString();
    		break;
    	case FormEntryController.EVENT_QUESTION:
    		value = "question." + index.getReference().toString();
    		break;
    	case FormEntryController.EVENT_PROMPT_NEW_REPEAT:
    		value = "promptNewRepeat." + index.getReference().toString();
    		break;
    	case FormEntryController.EVENT_REPEAT:
    		value = "repeat." + index.getReference().toString();
    		break;
    	case FormEntryController.EVENT_REPEAT_JUNCTURE:
    		value = "repeatJuncture." + index.getReference().toString();
    		break;
		default:
			value = "unexpected";
    		break;
    	}
    	return value;
    }

    public FormIndex getIndexFromXPath(String xPath) {
    	if ( xPath.equals("beginningOfForm") ) {
            return FormIndex.createBeginningOfFormIndex();
    	} else if ( xPath.equals("endOfForm") ) {
    		return FormIndex.createEndOfFormIndex();
    	} else if ( xPath.equals("unexpected") ) {
    		Log.e(t, "Unexpected string from XPath");
    		throw new IllegalArgumentException("unexpected string from XPath");
    	} else {
    		FormIndex returned = null;
			FormIndex saved = getFormIndex();
			// the only way I know how to do this is to step through the entire form
			// until the XPath of a form entry matches that of the supplied XPath
			try {
				jumpToIndex(FormIndex.createBeginningOfFormIndex());
				int event = stepToNextEvent(true);
				while ( event != FormEntryController.EVENT_END_OF_FORM ) {
					String candidateXPath = getXPath(getFormIndex());
					// Log.i(t, "xpath: " + candidateXPath);
					if ( candidateXPath.equals(xPath) ) {
						returned = getFormIndex();
						break;
					}
					event = stepToNextEvent(true);
				}
			} finally {
				jumpToIndex(saved);
			}
			return returned;
    	}
    }

    /**
     * returns the event for the current FormIndex.
     *
     * @return
     */
    public int getEvent() {
        return mFormEntryController.getModel().getEvent();
    }


    /**
     * returns the event for the given FormIndex.
     *
     * @param index
     * @return
     */
    public int getEvent(FormIndex index) {
        return mFormEntryController.getModel().getEvent(index);
    }


    /**
     * @return current FormIndex.
     */
    public FormIndex getFormIndex() {
        return mFormEntryController.getModel().getFormIndex();
    }


    /**
     * Return the langauges supported by the currently loaded form.
     *
     * @return Array of Strings containing the languages embedded in the XForm.
     */
    public String[] getLanguages() {
        return mFormEntryController.getModel().getLanguages();
    }


    /**
     * @return A String containing the title of the current form.
     */
    public String getFormTitle() {
        return mFormEntryController.getModel().getFormTitle();
    }


    /**
     * @return the currently selected language.
     */
    public String getLanguage() {
        return mFormEntryController.getModel().getLanguage();
    }

    public String getBindAttribute( String attributeNamespace, String attributeName) {
    	return getBindAttribute( getFormIndex(), attributeNamespace, attributeName );
    }

    public String getBindAttribute(FormIndex idx, String attributeNamespace, String attributeName) {
        return mFormEntryController.getModel().getForm().getMainInstance().resolveReference(
                idx.getReference()).getBindAttributeValue(attributeNamespace, attributeName);
    }

    /**
     * @return an array of FormEntryCaptions for the current FormIndex. This is how we get group
     *         information Group 1 > Group 2> etc... The element at [size-1] is the current question
     *         text, with group names decreasing in hierarchy until array element at [0] is the root
     */
    private FormEntryCaption[] getCaptionHierarchy() {
        return mFormEntryController.getModel().getCaptionHierarchy();
    }

    /**
     * @param index
     * @return an array of FormEntryCaptions for the supplied FormIndex. This is how we get group
     *         information Group 1 > Group 2> etc... The element at [size-1] is the current question
     *         text, with group names decreasing in hierarchy until array element at [0] is the root
     */
    private FormEntryCaption[] getCaptionHierarchy(FormIndex index) {
        return mFormEntryController.getModel().getCaptionHierarchy(index);
    }

    /**
     * Returns a caption prompt for the given index. This is used to create a multi-question per
     * screen view.
     *
     * @param index
     * @return
     */
    public FormEntryCaption getCaptionPrompt(FormIndex index) {
        return mFormEntryController.getModel().getCaptionPrompt(index);
    }


    /**
     * Return the caption for the current FormIndex. This is usually used for a repeat prompt.
     *
     * @return
     */
    public FormEntryCaption getCaptionPrompt() {
        return mFormEntryController.getModel().getCaptionPrompt();
    }


    /**
     * This fires off the jr:preload actions and events to save values like the
     * end time of a form.
     *
     * @return
     */
    public boolean postProcessInstance() {
        return mFormEntryController.getModel().getForm().postProcessInstance();
    }


    /**
     * TODO: We need a good description of what this does, exactly, and why.
     *
     * @return
     */
    private FormInstance getInstance() {
        return mFormEntryController.getModel().getForm().getInstance();
    }


    /**
     * A convenience method for determining if the current FormIndex is in a group that is/should be
     * displayed as a multi-question view. This is useful for returning from the formhierarchy view
     * to a selected index.
     *
     * @param index
     * @return
     */
    private boolean groupIsFieldList(FormIndex index) {
        // if this isn't a group, return right away
    	IFormElement element = mFormEntryController.getModel().getForm().getChild(index);
        if (!(element instanceof GroupDef)) {
            return false;
        }

        GroupDef gd = (GroupDef) element; // exceptions?
        return (ODKView.FIELD_LIST.equalsIgnoreCase(gd.getAppearanceAttr()));
    }

    private boolean repeatIsFieldList(FormIndex index) {
        // if this isn't a group, return right away
    	IFormElement element = mFormEntryController.getModel().getForm().getChild(index);
        if (!(element instanceof GroupDef)) {
            return false;
        }

        GroupDef gd = (GroupDef) element; // exceptions?
        return (ODKView.FIELD_LIST.equalsIgnoreCase(gd.getAppearanceAttr()));
    }

    /**
     * Tests if the FormIndex 'index' is located inside a group that is marked as a "field-list"
     *
     * @param index
     * @return true if index is in a "field-list". False otherwise.
     */
    private boolean indexIsInFieldList(FormIndex index) {
        int event = getEvent(index);
        if (event == FormEntryController.EVENT_QUESTION) {
            // caption[0..len-1]
            // caption[len-1] == the question itself
            // caption[len-2] == the first group it is contained in.
            FormEntryCaption[] captions = getCaptionHierarchy(index);
            if (captions.length < 2) {
                // no group
                return false;
            }
            FormEntryCaption grp = captions[captions.length - 2];
            return groupIsFieldList(grp.getIndex());
        } else if (event == FormEntryController.EVENT_GROUP) {
            return groupIsFieldList(index);
        } else if (event == FormEntryController.EVENT_REPEAT) {
        	return repeatIsFieldList(index);
        } else {
            // right now we only test Questions and Groups. Should we also handle
            // repeats?
            return false;
        }

    }

    public boolean currentPromptIsQuestion() {
        return (getEvent() == FormEntryController.EVENT_QUESTION
        		|| ((getEvent() == FormEntryController.EVENT_GROUP ||
        			 getEvent() == FormEntryController.EVENT_REPEAT)
        				&& indexIsInFieldList()));
    }

    /**
     * Tests if the current FormIndex is located inside a group that is marked as a "field-list"
     *
     * @return true if index is in a "field-list". False otherwise.
     */
    public boolean indexIsInFieldList() {
        return indexIsInFieldList(getFormIndex());
    }


    /**
     * Attempts to save answer at the current FormIndex into the data model.
     *
     * @param data
     * @return
     */
    private int answerQuestion(IAnswerData data) {
        return mFormEntryController.answerQuestion(data);
    }


    /**
     * Attempts to save answer into the given FormIndex into the data model.
     *
     * @param index
     * @param data
     * @return
     */
    public int answerQuestion(FormIndex index, IAnswerData data) throws JavaRosaException {
        try {
            return mFormEntryController.answerQuestion(index, data);
        } catch (Exception e) {
           throw new JavaRosaException(e);
        }
    }

    /**
     * Goes through the entire form to make sure all entered answers comply with their constraints.
     * Constraints are ignored on 'jump to', so answers can be outside of constraints. We don't
     * allow saving to disk, though, until all answers conform to their constraints/requirements.
     *
     *
     * @param markCompleted
     * @return ANSWER_OK and leave index unchanged or change index to bad value and return error type.
     */
    public int validateAnswers(Boolean markCompleted) {
        FormEntryController formEntryController = this.mFormEntryController;
        FormEntryModel formEntryModel = formEntryController.getModel();

        FormEntryModel formEntryModelToBeValidated = new FormEntryModel(formEntryModel.getForm());
        FormEntryController formEntryControllerToBeValidated = new FormEntryController(formEntryModelToBeValidated);
        FormController formControllerToBeValidated = new FormController(this.getMediaFolder(), formEntryControllerToBeValidated, this.getInstancePath());

        formControllerToBeValidated.jumpToIndex(FormIndex.createBeginningOfFormIndex());

        int event;
        while ((event =
                formControllerToBeValidated.stepToNextEvent(FormController.STEP_INTO_GROUP)) != FormEntryController.EVENT_END_OF_FORM) {
            if (event != FormEntryController.EVENT_QUESTION) {
                continue;
            } else {
                FormIndex formControllerToBeValidatedFormIndex = formControllerToBeValidated.getFormIndex();

                int saveStatus = formControllerToBeValidated.answerQuestion(formControllerToBeValidated.getQuestionPrompt().getAnswerValue());
                if (markCompleted && saveStatus != FormEntryController.ANSWER_OK) {
                    // jump to the error
                    this.jumpToIndex(formControllerToBeValidatedFormIndex);
                    return saveStatus;
                }
            }
        }

        return FormEntryController.ANSWER_OK;
    }


    /**
     * saveAnswer attempts to save the current answer into the data model without doing any
     * constraint checking. Only use this if you know what you're doing. For normal form filling you
     * should always use answerQuestion or answerCurrentQuestion.
     *
     * @param index
     * @param data
     * @return true if saved successfully, false otherwise.
     */
    public boolean saveAnswer(FormIndex index, IAnswerData data) throws JavaRosaException {
        try {
            return mFormEntryController.saveAnswer(index, data);
        } catch (Exception e) {
            throw new JavaRosaException(e);
        }
    }


    /**
     * saveAnswer attempts to save the current answer into the data model without doing any
     * constraint checking. Only use this if you know what you're doing. For normal form filling you
     * should always use answerQuestion().
     *
     * @param data
     * @return true if saved successfully, false otherwise.
     */
    public boolean saveAnswer(IAnswerData data) throws JavaRosaException {
        try {
            return mFormEntryController.saveAnswer(data);
        } catch (Exception e) {
            throw new JavaRosaException(e);
        }
    }


    /**
     * Navigates forward in the form.
     *
     * @return the next event that should be handled by a view.
     */
    public int stepToNextEvent(boolean stepIntoGroup) {
        if ((getEvent() == FormEntryController.EVENT_GROUP ||
        	 getEvent() == FormEntryController.EVENT_REPEAT)
                && indexIsInFieldList() && !stepIntoGroup) {
            return stepOverGroup();
        } else {
            return mFormEntryController.stepToNextEvent();
        }
    }


    /**
     * If using a view like HierarchyView that doesn't support multi-question per screen, step over
     * the group represented by the FormIndex.
     *
     * @return
     */
    private int stepOverGroup() {
        ArrayList<FormIndex> indicies = new ArrayList<FormIndex>();
        GroupDef gd =
            (GroupDef) mFormEntryController.getModel().getForm()
                    .getChild(getFormIndex());
        FormIndex idxChild =
            mFormEntryController.getModel().incrementIndex(
                getFormIndex(), true); // descend into group
        for (int i = 0; i < gd.getChildren().size(); i++) {
            indicies.add(idxChild);
            // don't descend
            idxChild = mFormEntryController.getModel().incrementIndex(idxChild, false);
        }

        // jump to the end of the group
        mFormEntryController.jumpToIndex(indicies.get(indicies.size() - 1));
        return stepToNextEvent(STEP_OVER_GROUP);
    }

    /**
     * used to go up one level in the formIndex. That is, if you're at 5_0, 1 (the second question
     * in a repeating group), this method will return a FormInex of 5_0 (the start of the repeating
     * group). If your at index 16 or 5_0, this will return null;
     *
     * @param index
     * @return index
     */
    public FormIndex stepIndexOut(FormIndex index) {
        if (index.isTerminal()) {
            return null;
        } else {
            return new FormIndex(stepIndexOut(index.getNextLevel()), index);
        }
    }

    /**
     * Move the current form index to the index of the previous question in the form.
     * Step backward out of repeats and groups as needed. If the resulting question
     * is itself within a field-list, move upward to the group or repeat defining that
     * field-list.
     *
     * @return
     */
    public int stepToPreviousScreenEvent() throws JavaRosaException {
        try {
            if (getEvent() != FormEntryController.EVENT_BEGINNING_OF_FORM) {
                int event = stepToPreviousEvent();

                while (event == FormEntryController.EVENT_REPEAT_JUNCTURE ||
                       event == FormEntryController.EVENT_PROMPT_NEW_REPEAT ||
                       (event == FormEntryController.EVENT_QUESTION && indexIsInFieldList()) ||
                       ((event == FormEntryController.EVENT_GROUP
                         || event == FormEntryController.EVENT_REPEAT) && !indexIsInFieldList())) {
                    event = stepToPreviousEvent();
                }

                // Work-around for broken field-list handling from 1.1.7 which breaks either
                // build-generated forms or XLSForm-generated forms.  If the current group
                // is a GROUP with field-list and it is nested within a group or repeat with just
                // this containing group, and that is also a field-list, then return the parent group.
                if ( getEvent() == FormEntryController.EVENT_GROUP ) {
                    FormIndex currentIndex = getFormIndex();
                    IFormElement element = mFormEntryController.getModel().getForm().getChild(currentIndex);
                    if (element instanceof GroupDef) {
                        GroupDef gd = (GroupDef) element;
                        if ( ODKView.FIELD_LIST.equalsIgnoreCase(gd.getAppearanceAttr()) ) {
                            // OK this group is a field-list... see what the parent is...
                            FormEntryCaption[] fclist = this.getCaptionHierarchy(currentIndex);
                            if ( fclist.length > 1) {
                                FormEntryCaption fc = fclist[fclist.length-2];
                                GroupDef pd = (GroupDef) fc.getFormElement();
                                if ( pd.getChildren().size() == 1 &&
                                     ODKView.FIELD_LIST.equalsIgnoreCase(pd.getAppearanceAttr()) ) {
                                    mFormEntryController.jumpToIndex(fc.getIndex());
                                }
                            }
                        }
                    }
                }
            }
            return getEvent();
        } catch (RuntimeException e) {
            throw new JavaRosaException(e);
        }
    }

    /**
     * Move the current form index to the index of the next question in the form.
     * Stop if we should ask to create a new repeat group or if we reach the end of the form.
     * If we enter a group or repeat, return that if it is a field-list definition.
     * Otherwise, descend into the group or repeat searching for the first question.
     *
     * @return
     */
    public int stepToNextScreenEvent() throws JavaRosaException {
        try {
            if (getEvent() != FormEntryController.EVENT_END_OF_FORM) {
                int event;
                group_skip: do {
                    event = stepToNextEvent(FormController.STEP_OVER_GROUP);
                    switch (event) {
                        case FormEntryController.EVENT_QUESTION:
                        case FormEntryController.EVENT_END_OF_FORM:
                            break group_skip;
                        case FormEntryController.EVENT_PROMPT_NEW_REPEAT:
                            break group_skip;
                        case FormEntryController.EVENT_GROUP:
                        case FormEntryController.EVENT_REPEAT:
                            if (indexIsInFieldList()
                                    && getQuestionPrompts().length != 0) {
                                break group_skip;
                            }
                            // otherwise it's not a field-list group, so just skip it
                            break;
                        case FormEntryController.EVENT_REPEAT_JUNCTURE:
                            Log.i(t, "repeat juncture: "
                                    + getFormIndex().getReference());
                            // skip repeat junctures until we implement them
                            break;
                        default:
                            Log.w(t,
                                "JavaRosa added a new EVENT type and didn't tell us... shame on them.");
                            break;
                    }
                } while (event != FormEntryController.EVENT_END_OF_FORM);
            }
            return getEvent();
        } catch (RuntimeException e) {
            throw new JavaRosaException(e);
        }
    }


    /**
     * Move the current form index to the index of the first enclosing repeat
     * or to the start of the form.
     *
     * @return
     */
    public int stepToOuterScreenEvent() {
        FormIndex index = stepIndexOut(getFormIndex());
        int currentEvent = getEvent();

        // Step out of any group indexes that are present.
        while (index != null
                && getEvent(index) == FormEntryController.EVENT_GROUP) {
            index = stepIndexOut(index);
        }

        if (index == null) {
            jumpToIndex(FormIndex.createBeginningOfFormIndex());
        } else {
            if (currentEvent == FormEntryController.EVENT_REPEAT) {
                // We were at a repeat, so stepping back brought us to then previous level
                jumpToIndex(index);
            } else {
                // We were at a question, so stepping back brought us to either:
                // The beginning. or The start of a repeat. So we need to step
                // out again to go passed the repeat.
                index = stepIndexOut(index);
                if (index == null) {
                    jumpToIndex(FormIndex.createBeginningOfFormIndex());
                } else {
                    jumpToIndex(index);
                }
            }
        }
        return getEvent();
    }


    public static class FailedConstraint {
    	public final FormIndex index;
    	public final int status;

    	FailedConstraint(FormIndex index, int status) {
    		this.index = index;
    		this.status = status;
    	}
    }
    /**
     *
     * @param answers
     * @param evaluateConstraints
     * @return FailedConstraint of first failed constraint or null if all questions were saved.
     */
    public FailedConstraint saveAllScreenAnswers(LinkedHashMap<FormIndex,IAnswerData> answers, boolean evaluateConstraints) throws JavaRosaException {
    	if (currentPromptIsQuestion()) {
            Iterator<FormIndex> it = answers.keySet().iterator();
            while (it.hasNext()) {
                FormIndex index = it.next();
                // Within a group, you can only save for question events
                if (getEvent(index) == FormEntryController.EVENT_QUESTION) {
                	int saveStatus;
                	IAnswerData answer = answers.get(index);
                	if (evaluateConstraints) {
                		saveStatus = answerQuestion(index, answer);
                        if (saveStatus != FormEntryController.ANSWER_OK) {
                            return new FailedConstraint(index, saveStatus);
                        }
                    } else {
                        saveAnswer(index, answer);
                    }
                } else {
                    Log.w(t,
                        "Attempted to save an index referencing something other than a question: "
                                + index.getReference());
                }
            }
        }
    	return null;
    }


    /**
     * Navigates backward in the form.
     *
     * @return the event that should be handled by a view.
     */
    public int stepToPreviousEvent() {
        /*
         * Right now this will always skip to the beginning of a group if that group is represented
         * as a 'field-list'. Should a need ever arise to step backwards by only one step in a
         * 'field-list', this method will have to be updated.
         */

        mFormEntryController.stepToPreviousEvent();

        // If after we've stepped, we're in a field-list, jump back to the beginning of the group
        //

        if (indexIsInFieldList()
                && getEvent() == FormEntryController.EVENT_QUESTION) {
            // caption[0..len-1]
            // caption[len-1] == the question itself
            // caption[len-2] == the first group it is contained in.
            FormEntryCaption[] captions = getCaptionHierarchy();
            FormEntryCaption grp = captions[captions.length - 2];
            int event = mFormEntryController.jumpToIndex(grp.getIndex());
            // and test if this group or at least one of its children is relevant...
            FormIndex idx = grp.getIndex();
            if ( !mFormEntryController.getModel().isIndexRelevant(idx) ) {
            	return stepToPreviousEvent();
            }
            idx = mFormEntryController.getModel().incrementIndex(idx, true);
            while ( FormIndex.isSubElement(grp.getIndex(), idx) ) {
            	if ( mFormEntryController.getModel().isIndexRelevant(idx) ) {
            		return event;
            	}
                idx = mFormEntryController.getModel().incrementIndex(idx, true);
            }
            return stepToPreviousEvent();
        } else if ( indexIsInFieldList() && getEvent() == FormEntryController.EVENT_GROUP) {
            FormIndex grpidx = mFormEntryController.getModel().getFormIndex();
            int event = mFormEntryController.getModel().getEvent();
            // and test if this group or at least one of its children is relevant...
            if ( !mFormEntryController.getModel().isIndexRelevant(grpidx) ) {
            	return stepToPreviousEvent(); // shouldn't happen?
            }
            FormIndex idx = mFormEntryController.getModel().incrementIndex(grpidx, true);
            while ( FormIndex.isSubElement(grpidx, idx) ) {
            	if ( mFormEntryController.getModel().isIndexRelevant(idx) ) {
            		return event;
            	}
                idx = mFormEntryController.getModel().incrementIndex(idx, true);
            }
            return stepToPreviousEvent();
        }

        return getEvent();

    }


    /**
     * Jumps to a given FormIndex.
     *
     * @param index
     * @return EVENT for the specified Index.
     */
    public int jumpToIndex(FormIndex index) {
        return mFormEntryController.jumpToIndex(index);
    }


    /**
     * Creates a new repeated instance of the group referenced by the current FormIndex.
     *
     * @param questionIndex
     */
    public void newRepeat() {
        mFormEntryController.newRepeat();
    }


    /**
     * If the current FormIndex is within a repeated group, will find the innermost repeat, delete
     * it, and jump the FormEntryController to the previous valid index. That is, if you have group1
     * (2) > group2 (3) and you call deleteRepeat, it will delete the 3rd instance of group2.
     */
    public void deleteRepeat() {
        FormIndex fi = mFormEntryController.deleteRepeat();
        mFormEntryController.jumpToIndex(fi);
    }


    /**
     * Sets the current language.
     *
     * @param language
     */
    public void setLanguage(String language) {
        mFormEntryController.setLanguage(language);
    }


    /**
     * Returns an array of question promps.
     *
     * @return
     */
    public FormEntryPrompt[] getQuestionPrompts() throws RuntimeException {

        ArrayList<FormIndex> indicies = new ArrayList<FormIndex>();
        FormIndex currentIndex = getFormIndex();

        // For questions, there is only one.
        // For groups, there could be many, but we set that below
        FormEntryPrompt[] questions = new FormEntryPrompt[1];

    	IFormElement element = mFormEntryController.getModel().getForm().getChild(currentIndex);
        if (element instanceof GroupDef) {
            GroupDef gd = (GroupDef) element;
            // descend into group
            FormIndex idxChild = mFormEntryController.getModel().incrementIndex(currentIndex, true);

            if ( gd.getChildren().size() == 1 && getEvent(idxChild) == FormEntryController.EVENT_GROUP ) {
            	// if we have a group definition within a field-list attribute group, and this is the
            	// only child in the group, check to see if it is also a field-list appearance.
            	// If it is, then silently recurse into it to pick up its elements.
            	// Work-around for the inconsistent treatment of field-list groups and repeats in 1.1.7 that
            	// either breaks forms generated by build or breaks forms generated by XLSForm.
            	IFormElement nestedElement = mFormEntryController.getModel().getForm().getChild(idxChild);
                if (nestedElement instanceof GroupDef) {
                    GroupDef nestedGd = (GroupDef) nestedElement;
                    if ( ODKView.FIELD_LIST.equalsIgnoreCase(nestedGd.getAppearanceAttr()) ) {
                    	gd = nestedGd;
                    	idxChild = mFormEntryController.getModel().incrementIndex(idxChild, true);
                    }
                }
            }

            for (int i = 0; i < gd.getChildren().size(); i++) {
                indicies.add(idxChild);
                // don't descend
                idxChild = mFormEntryController.getModel().incrementIndex(idxChild, false);
            }

            // we only display relevant questions
            ArrayList<FormEntryPrompt> questionList = new ArrayList<FormEntryPrompt>();
            for (int i = 0; i < indicies.size(); i++) {
                FormIndex index = indicies.get(i);

                if (getEvent(index) != FormEntryController.EVENT_QUESTION) {
                    String errorMsg =
                        "Only questions are allowed in 'field-list'.  Bad node is: "
                                + index.getReference().toString(false);
                    RuntimeException e = new RuntimeException(errorMsg);
                    Log.e(t, errorMsg);
                    throw e;
                }

                // we only display relevant questions
                if (mFormEntryController.getModel().isIndexRelevant(index)) {
                    questionList.add(getQuestionPrompt(index));
                }
                questions = new FormEntryPrompt[questionList.size()];
                questionList.toArray(questions);
            }
        } else {
            // We have a quesion, so just get the one prompt
            questions[0] = getQuestionPrompt();
        }

        return questions;
    }


    public FormEntryPrompt getQuestionPrompt(FormIndex index) {
        return mFormEntryController.getModel().getQuestionPrompt(index);
    }


    public FormEntryPrompt getQuestionPrompt() {
        return mFormEntryController.getModel().getQuestionPrompt();
    }

    public String getQuestionPromptConstraintText(FormIndex index) {
    	return mFormEntryController.getModel().getQuestionPrompt(index).getConstraintText();
    }

    public String getQuestionPromptRequiredText(FormIndex index) {
    	// look for the text under the requiredMsg bind attribute
		String constraintText = getBindAttribute(index, XFormParser.NAMESPACE_JAVAROSA, "requiredMsg");
		if (constraintText != null) {
	    	XPathExpression xPathRequiredMsg;
			try {
				xPathRequiredMsg = XPathParseTool.parseXPath("string(" + constraintText + ")");
			} catch(Exception e) {
				// Expected in probably most cases.
                // This is a string literal, so no need to evaluate anything.
                return constraintText;
			}

			if(xPathRequiredMsg != null) {
				try{
					FormDef form = mFormEntryController.getModel().getForm();
					TreeElement mTreeElement = form.getMainInstance().resolveReference(index.getReference());
					EvaluationContext ec = new EvaluationContext(form.exprEvalContext, mTreeElement.getRef());
					Object value = xPathRequiredMsg.eval(form.getMainInstance(), ec);
					if(value != "") {
						return (String)value;
					}
					return null;
				} catch(Exception e) {
					Log.e(t,"Error evaluating a valid-looking required xpath ", e);
					return constraintText;
				}
			} else {
				return constraintText;
			}
		}
		return null;
    }

    /**
     * Returns an array of FormEntryCaptions for current FormIndex.
     *
     * @return
     */
    public FormEntryCaption[] getGroupsForCurrentIndex() {
        // return an empty array if you ask for something impossible
        if (!(getEvent() == FormEntryController.EVENT_QUESTION
                || getEvent() == FormEntryController.EVENT_PROMPT_NEW_REPEAT
                || getEvent() == FormEntryController.EVENT_GROUP
                || getEvent() == FormEntryController.EVENT_REPEAT)) {
            return new FormEntryCaption[0];
        }

        // the first caption is the question, so we skip it if it's an EVENT_QUESTION
        // otherwise, the first caption is a group so we start at index 0
        int lastquestion = 1;
        if (getEvent() == FormEntryController.EVENT_PROMPT_NEW_REPEAT
                || getEvent() == FormEntryController.EVENT_GROUP
                || getEvent() == FormEntryController.EVENT_REPEAT) {
            lastquestion = 0;
        }

        FormEntryCaption[] v = getCaptionHierarchy();
        FormEntryCaption[] groups = new FormEntryCaption[v.length - lastquestion];
        for (int i = 0; i < v.length - lastquestion; i++) {
            groups[i] = v[i];
        }
        return groups;
    }


    /**
     * This is used to enable/disable the "Delete Repeat" menu option.
     *
     * @return
     */
    public boolean indexContainsRepeatableGroup() {
        FormEntryCaption[] groups = getCaptionHierarchy();
        if (groups.length == 0) {
            return false;
        }
        for (int i = 0; i < groups.length; i++) {
            if (groups[i].repeats())
                return true;
        }
        return false;
    }


    /**
     * The count of the closest group that repeats or -1.
     */
    public int getLastRepeatedGroupRepeatCount() {
        FormEntryCaption[] groups = getCaptionHierarchy();
        if (groups.length > 0) {
            for (int i = groups.length - 1; i > -1; i--) {
                if (groups[i].repeats()) {
                    return groups[i].getMultiplicity();

                }
            }
        }
        return -1;
    }


    /**
     * The name of the closest group that repeats or null.
     */
    public String getLastRepeatedGroupName() {
        FormEntryCaption[] groups = getCaptionHierarchy();
        // no change
        if (groups.length > 0) {
            for (int i = groups.length - 1; i > -1; i--) {
                if (groups[i].repeats()) {
                    return groups[i].getLongText();
                }
            }
        }
        return null;
    }


    /**
     * The closest group the prompt belongs to.
     *
     * @return FormEntryCaption
     */
    private FormEntryCaption getLastGroup() {
        FormEntryCaption[] groups = getCaptionHierarchy();
        if (groups == null || groups.length == 0)
            return null;
        else
            return groups[groups.length - 1];
    }


    /**
     * The repeat count of closest group the prompt belongs to.
     */
    public int getLastRepeatCount() {
        if (getLastGroup() != null) {
            return getLastGroup().getMultiplicity();
        }
        return -1;

    }


    /**
     * The text of closest group the prompt belongs to.
     */
    public String getLastGroupText() {
        if (getLastGroup() != null) {
            return getLastGroup().getLongText();
        }
        return null;
    }

    /**
     * Find the portion of the form that is to be submitted
     *
     * @return
     */
    private IDataReference getSubmissionDataReference() {
        FormDef formDef = mFormEntryController.getModel().getForm();
        // Determine the information about the submission...
        SubmissionProfile p = formDef.getSubmissionProfile();
        if (p == null || p.getRef() == null) {
            return new XPathReference("/");
        } else {
            return p.getRef();
        }
    }

    /**
     * Once a submission is marked as complete, it is saved in the
     * submission format, which might be a fragment of the original
     * form or might be a SMS text string, etc.
     *
     * @return true if the submission is the entire form.  If it is,
     *              then the submission can be re-opened for editing
     *              after it was marked-as-complete (provided it has
     *              not been encrypted).
     */
    public boolean isSubmissionEntireForm() {
        IDataReference sub = getSubmissionDataReference();
        return ( getInstance().resolveReference(sub) == null );
    }

    /**
     * Constructs the XML payload for a filled-in form instance. This payload
     * enables a filled-in form to be re-opened and edited.
     *
     * @return
     * @throws IOException
     */
    public ByteArrayPayload getFilledInFormXml() throws IOException {
        // assume no binary data inside the model.
        FormInstance datamodel = getInstance();
        XFormSerializingVisitor serializer = new XFormSerializingVisitor();
        ByteArrayPayload payload =
        		(ByteArrayPayload) serializer.createSerializedPayload(datamodel);

        return payload;
    }

    /**
     * Extract the portion of the form that should be uploaded to the server.
     *
     * @return
     * @throws IOException
     */
    public ByteArrayPayload getSubmissionXml() throws IOException {
        FormInstance instance = getInstance();
        XFormSerializingVisitor serializer = new XFormSerializingVisitor();
        ByteArrayPayload payload =
                (ByteArrayPayload) serializer.createSerializedPayload(instance,
                                                   getSubmissionDataReference());
        return payload;
    }

    /**
     * Traverse the submission looking for the first matching tag in depth-first order.
     *
     * @param parent
     * @param name
     * @return
     */
    private TreeElement findDepthFirst(TreeElement parent, String name) {
        int len = parent.getNumChildren();
        for ( int i = 0; i < len ; ++i ) {
            TreeElement e = parent.getChildAt(i);
            if ( name.equals(e.getName()) ) {
                return e;
            } else if ( e.getNumChildren() != 0 ) {
                TreeElement v = findDepthFirst(e, name);
                if ( v != null ) return v;
            }
        }
        return null;
    }

    /**
     * Get the OpenRosa required metadata of the portion of the form beng submitted
     * @return
     */
    public InstanceMetadata getSubmissionMetadata() {
        FormDef formDef = mFormEntryController.getModel().getForm();
        TreeElement rootElement = formDef.getInstance().getRoot();

        TreeElement trueSubmissionElement;
        // Determine the information about the submission...
        SubmissionProfile p = formDef.getSubmissionProfile();
        if ( p == null || p.getRef() == null ) {
            trueSubmissionElement = rootElement;
        } else {
            IDataReference ref = p.getRef();
            trueSubmissionElement = formDef.getInstance().resolveReference(ref);
            // resolveReference returns null if the reference is to the root element...
            if ( trueSubmissionElement == null ) {
                trueSubmissionElement = rootElement;
            }
        }

        // and find the depth-first meta block in this...
        TreeElement e = findDepthFirst(trueSubmissionElement, "meta");

        String instanceId = null;
        String instanceName = null;

        if ( e != null ) {
            Vector<TreeElement> v;

            // instance id...
            v = e.getChildrenWithName(INSTANCE_ID);
            if ( v.size() == 1 ) {
                StringData sa = (StringData) v.get(0).getValue();
                if ( sa != null ) {
                	instanceId = (String) sa.getValue();
                }
            }

            // instance name...
            v = e.getChildrenWithName(INSTANCE_NAME);
            if ( v.size() == 1 ) {
                StringData sa = (StringData) v.get(0).getValue();
                if ( sa != null ) {
                    instanceName = (String) sa.getValue();
                }
            }
        }

        return new InstanceMetadata(instanceId,instanceName);
    }

}
