/*******************************************************************************
 * Copyright (c) 2001, 2011 IBM Corporation and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *     Jens Lukowski/Innoopract - initial renaming/restructuring
 *     
 *******************************************************************************/
package org.eclipse.wst.sse.core.internal.model;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;

import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.jobs.ILock;
import org.eclipse.wst.sse.core.StructuredModelManager;
import org.eclipse.wst.sse.core.internal.Logger;
import org.eclipse.wst.sse.core.internal.SSECoreMessages;
import org.eclipse.wst.sse.core.internal.encoding.EncodingRule;
import org.eclipse.wst.sse.core.internal.ltk.modelhandler.IModelHandler;
import org.eclipse.wst.sse.core.internal.provisional.DocumentChanged;
import org.eclipse.wst.sse.core.internal.provisional.IModelLifecycleListener;
import org.eclipse.wst.sse.core.internal.provisional.IModelManager;
import org.eclipse.wst.sse.core.internal.provisional.IModelStateListener;
import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel;
import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion;
import org.eclipse.wst.sse.core.internal.provisional.events.AboutToBeChangedEvent;
import org.eclipse.wst.sse.core.internal.provisional.events.IModelAboutToBeChangedListener;
import org.eclipse.wst.sse.core.internal.provisional.events.IStructuredDocumentListener;
import org.eclipse.wst.sse.core.internal.provisional.events.NewDocumentEvent;
import org.eclipse.wst.sse.core.internal.provisional.events.NoChangeEvent;
import org.eclipse.wst.sse.core.internal.provisional.events.RegionChangedEvent;
import org.eclipse.wst.sse.core.internal.provisional.events.RegionsReplacedEvent;
import org.eclipse.wst.sse.core.internal.provisional.events.StructuredDocumentRegionsReplacedEvent;
import org.eclipse.wst.sse.core.internal.provisional.exceptions.ResourceInUse;
import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument;
import org.eclipse.wst.sse.core.internal.undo.IStructuredTextUndoManager;
import org.eclipse.wst.sse.core.internal.util.URIResolver;
import org.eclipse.wst.sse.core.internal.util.Utilities;


public abstract class AbstractStructuredModel implements IStructuredModel {

	private static final String MODEL_MANAGER_NULL = "Warning: AbstractStructuredModel::close:  model manager was null during a close of a model (which should be impossible)"; //$NON-NLS-1$

	class DirtyStateWatcher implements IStructuredDocumentListener {

		public void newModel(NewDocumentEvent structuredDocumentEvent) {

			// I don't think its safe to assume a new model
			// is always "fresh", so we'll leave dirty state
			// unchanged;
			// but we'll tell everyone about it.
			setDirtyState(fDirtyState);
		}

		public void noChange(NoChangeEvent structuredDocumentEvent) {

			// don't change dirty state
		}

		public void nodesReplaced(StructuredDocumentRegionsReplacedEvent structuredDocumentEvent) {

			setDirtyState(true);
			// no need to listen any more
			if (fStructuredDocument != null) {
				fStructuredDocument.removeDocumentChangedListener(fDirtyStateWatcher);
			}
		}

		public void regionChanged(RegionChangedEvent structuredDocumentEvent) {

			setDirtyState(true);
			// no need to listen any more
			if (fStructuredDocument != null) {
				fStructuredDocument.removeDocumentChangedListener(fDirtyStateWatcher);
			}
		}

		public void regionsReplaced(RegionsReplacedEvent structuredDocumentEvent) {

			setDirtyState(true);
			// no need to listen any more
			if (fStructuredDocument != null) {
				fStructuredDocument.removeDocumentChangedListener(fDirtyStateWatcher);
			}
		}
	}

	class DocumentToModelNotifier implements IStructuredDocumentListener, IModelAboutToBeChangedListener {

		/*
		 * (non-Javadoc)
		 * 
		 * @see org.eclipse.wst.sse.core.events.IModelAboutToBeChangedListener#modelAboutToBeChanged(org.eclipse.wst.sse.core.events.AboutToBeChangedEvent)
		 */
		public void modelAboutToBeChanged(AboutToBeChangedEvent structuredDocumentEvent) {
			// If we didn't originate the change, take note we are about to
			// change based on our underlying document changing.
			// If we did originate the change, we, or client, should have
			// already called aboutToChangeModel.
			if (structuredDocumentEvent.getOriginalRequester() != this) {
				aboutToChangeModel();
			}

		}

		/*
		 * (non-Javadoc)
		 * 
		 * @see org.eclipse.wst.sse.core.events.IStructuredDocumentListener#newModel(org.eclipse.wst.sse.core.events.NewDocumentEvent)
		 */
		public void newModel(NewDocumentEvent structuredDocumentEvent) {
			// if we didn't originate the change, take note we have changed
			if (structuredDocumentEvent.getOriginalRequester() != this) {
				changedModel();
			}
		}

		/*
		 * (non-Javadoc)
		 * 
		 * @see org.eclipse.wst.sse.core.events.IStructuredDocumentListener#noChange(org.eclipse.wst.sse.core.events.NoChangeEvent)
		 */
		public void noChange(NoChangeEvent structuredDocumentEvent) {
			// if we didn't originate the change, take note we have changed
			if (structuredDocumentEvent.getOriginalRequester() != this) {
				changedModel();
			}
		}

		/*
		 * (non-Javadoc)
		 * 
		 * @see org.eclipse.wst.sse.core.events.IStructuredDocumentListener#nodesReplaced(org.eclipse.wst.sse.core.events.StructuredDocumentRegionsReplacedEvent)
		 */
		public void nodesReplaced(StructuredDocumentRegionsReplacedEvent structuredDocumentEvent) {
			// if we didn't originate the change, take note we have changed
			if (structuredDocumentEvent.getOriginalRequester() != this) {
				changedModel();
			}
		}

		/*
		 * (non-Javadoc)
		 * 
		 * @see org.eclipse.wst.sse.core.events.IStructuredDocumentListener#regionChanged(org.eclipse.wst.sse.core.events.RegionChangedEvent)
		 */
		public void regionChanged(RegionChangedEvent structuredDocumentEvent) {
			// if we didn't originate the change, take note we have changed
			if (structuredDocumentEvent.getOriginalRequester() != this) {
				changedModel();
			}
		}

		/*
		 * (non-Javadoc)
		 * 
		 * @see org.eclipse.wst.sse.core.events.IStructuredDocumentListener#regionsReplaced(org.eclipse.wst.sse.core.events.RegionsReplacedEvent)
		 */
		public void regionsReplaced(RegionsReplacedEvent structuredDocumentEvent) {
			// if we didn't originate the change, take note we have changed
			if (structuredDocumentEvent.getOriginalRequester() != this) {
				changedModel();
			}
		}

	}

	private FactoryRegistry factoryRegistry;
	private String fBaseLocation;
	boolean fDirtyState;
	DirtyStateWatcher fDirtyStateWatcher;
	DocumentToModelNotifier fDocumentToModelNotifier;
	private String fExplicitContentTypeIdentifier;
	private String fId;

	private LifecycleNotificationManager fLifecycleNotificationManager;

	private final Object fListenerLock = new byte[0];
	protected ILock fLockObject;
	// private String fLineDelimiter;
	// private Object fType;
	private IModelHandler fModelHandler;
	// issue: we should not "hold on" to model manager, can 
	// easily get with StructuredModelManager.getModelManager();
	// but will need to add more null checks.
	private IModelManager fModelManager;
	private int fModelStateChanging;
	private Object[] fModelStateListeners;
	private boolean fNewState = false;
	private URIResolver fResolver;
	protected IStructuredDocument fStructuredDocument;
	/**
	 * The time stamp of the underlying resource's modification date, at the
	 * time this model was created, or the last time it was saved. Note: for
	 * this version, this variable is not set automatically, be needs to be
	 * managed by client. The FileModelProvider does this for most cases, but
	 * if client do not use FileModelProvider, they must set this variable
	 */
	public long fSynchronizationStamp = IResource.NULL_STAMP;
	private boolean reinitializationNeeded;
	private Object reinitializeStateData;

	/**
	 * AbstractStructuredModel constructor comment.
	 */
	public AbstractStructuredModel() {

		super();
		fDirtyStateWatcher = new DirtyStateWatcher();
		fDocumentToModelNotifier = new DocumentToModelNotifier();
	}


	/**
	 * This method is just for getting an instance of the model manager of the
	 * right Impl type, to be used "internally" for making protected calls
	 * directly to the impl class.
	 */
	private ModelManagerImpl _getModelManager() {
		// TODO_future: redesign so we don't need this 'Impl' version
		if (fModelManager == null) {
			fModelManager = StructuredModelManager.getModelManager();
		}

		return (ModelManagerImpl) fModelManager;
	}

	/**
	 * This API allows clients to declare that they are about to make a
	 * "large" change to the model. This change might be in terms of content
	 * or it might be in terms of the model id or base location. Note that in
	 * the case of embedded calls, notification to listeners is sent only
	 * once. Note that the client who is making these changes has the
	 * responsibility to restore the models state once finished with the
	 * changes. See getMemento and restoreState. The method
	 * isModelStateChanging can be used by a client to determine if the model
	 * is already in a change sequence.
	 */
	public void aboutToChangeModel() {


		// notice this is just a public avenue to our protected method
		internalAboutToBeChanged();
	}


	public void aboutToReinitializeModel() {



		// notice this is just a public avenue to our protected method
		fireModelAboutToBeReinitialized();
	}


	public void addModelLifecycleListener(IModelLifecycleListener listener) {

		synchronized (fListenerLock) {
			if (fLifecycleNotificationManager == null) {
				fLifecycleNotificationManager = new LifecycleNotificationManager();
			}
			fLifecycleNotificationManager.addListener(listener);
		}
	}

	public void addModelStateListener(IModelStateListener listener) {

		synchronized (fListenerLock) {

			if (!Utilities.contains(fModelStateListeners, listener)) {
				int oldSize = 0;
				if (fModelStateListeners != null) {
					// normally won't be null, but we need to be sure, for
					// first
					// time through
					oldSize = fModelStateListeners.length;
				}
				int newSize = oldSize + 1;
				Object[] newListeners = new Object[newSize];
				if (fModelStateListeners != null) {
					System.arraycopy(fModelStateListeners, 0, newListeners, 0, oldSize);
				}
				// add listener to last position
				newListeners[newSize - 1] = listener;
				//
				// now switch new for old
				fModelStateListeners = newListeners;
			}
		}
	}

	/**
	 * This lock to lock the small bits of data and operations in the models
	 * themselves. this lock is "shared" with document, so, eventually,
	 * changes can be made safely from either side.
	 * 
	 * @deprecated
	 */
	protected final void beginLock() {
	}

	public void beginRecording(Object requester) {

		beginRecording(requester, null, null);
	}

	public void beginRecording(Object requester, int cursorPosition, int selectionLength) {

		beginRecording(requester, null, null, cursorPosition, selectionLength);
	}

	public void beginRecording(Object requester, String label) {

		beginRecording(requester, label, null);
	}

	public void beginRecording(Object requester, String label, int cursorPosition, int selectionLength) {

		beginRecording(requester, label, null, cursorPosition, selectionLength);
	}

	public void beginRecording(Object requester, String label, String description) {

		if (getUndoManager() != null)
			getUndoManager().beginRecording(requester, label, description);
	}

	public void beginRecording(Object requester, String label, String description, int cursorPosition, int selectionLength) {

		if (getUndoManager() != null)
			getUndoManager().beginRecording(requester, label, description, cursorPosition, selectionLength);
	}

	/**
	 * This API allows a client controlled way of notifying all ModelEvent
	 * listners that the model has been changed. This method is a matched pair
	 * to aboutToChangeModel, and *must* be called after aboutToChangeModel
	 * ... or some listeners could be left waiting indefinitely for the
	 * changed event. So, its suggested that changedModel always be in a
	 * finally clause. Likewise, a client should never call changedModel
	 * without calling aboutToChangeModel first. In the case of embedded
	 * calls, the notification is just sent once.
	 */
	public void changedModel() {


		// notice this is just a public avenue to our protected method
		internalModelChanged();
		// also note!
		// if we've been "changed" by a client, we might still need
		// to be re-initialized, so we'll check and handle that here.
		// Note only does this provide a solution to some "missed"
		// re-inits, in provides a built in way for clients to
		// "force" the model to handle itself, by bracketing any
		// changes with aboutToChange and changed, the model itself
		// will check. But only call re-init if all other pending
		// modelChanged states have been handled.
		if (fModelStateChanging == 0 && isReinitializationNeeded()) {
			reinit();
		}
	}


	/**
	 * Based on similar method in FileDocumentProvider. It will provide what
	 * the modificationStamp would be if resetSynchronzationStamp(resource)
	 * were used, although for this 'compute' API, no changes to the instance
	 * are made.
	 */
	public long computeModificationStamp(IResource resource) {


		long modificationStamp = resource.getModificationStamp();
		IPath path = resource.getLocation();
		if (path == null) {
			return modificationStamp;
		}
		// Note: checking existence of file is a little different than
		// impl in
		// the FileDocumentProvider. See defect number 223790.
		File file = path.toFile();
		if (!file.exists()) {
			return modificationStamp;
		}
		modificationStamp = file.lastModified();
		return modificationStamp;
	}


	/**
	 * Provides a copy of the model, but a new ID must be provided. The
	 * principle of this copy is not to copy fields, etc., as is typically
	 * done in a clone method, but to return a model with the same content in
	 * the structuredDocument. Note: It is the callers responsibility to
	 * setBaseLocation, listners, etc., as appropriate. Type and Encoding are
	 * the only fields set by this method. If the newId provided already exist
	 * in the model manager, a ResourceInUse exception is thrown.
	 */
	public IStructuredModel copy(String newId) throws ResourceInUse {


		IStructuredModel newModel = null;
		// this first one should fail, if not, its treated as an error
		// If the caller wants to use an existing one, they can call
		// getExisting
		// after this failure
		newModel = getModelManager().getExistingModelForEdit(newId);
		if (newModel != null) {
			// be sure to release the reference we got "by accident" (and
			// no
			// longer need)
			newModel.releaseFromEdit();
			throw new ResourceInUse();
		}
		newModel = getModelManager().copyModelForEdit(getId(), newId);
		return newModel;
	}


	/**
	 * Disable undo management.
	 */
	public void disableUndoManagement() {

		if (getUndoManager() != null)
			getUndoManager().disableUndoManagement();
	}

	/**
	 * Enable undo management.
	 */
	public void enableUndoManagement() {

		if (getUndoManager() != null)
			getUndoManager().enableUndoManagement();
	}

	/**
	 * endLock is protected only for a very special purpose. So subclasses can
	 * call it to end the lock after updates have been made, but before
	 * notifications are sent
	 * 
	 * @deprecated
	 */
	protected final void endLock() {
	}

	public void endRecording(Object requester) {

		if (getUndoManager() != null)
			getUndoManager().endRecording(requester);
	}

	public void endRecording(Object requester, int cursorPosition, int selectionLength) {

		if (getUndoManager() != null)
			getUndoManager().endRecording(requester, cursorPosition, selectionLength);
	}

	/**
	 * Informs all registered model state listeners that the the model is
	 * about to under go a change. This change might be in terms of contents
	 * or might be in terms of the model's id or base location.
	 */
	private void fireModelAboutToBeChanged() {

		// we must assign listeners to local variable, since the add and
		// remove listner
		// methods can change the actual instance of the listener array
		// from another thread
		if (fModelStateListeners != null) {
			Object[] holdListeners = fModelStateListeners;
			for (int i = 0; i < holdListeners.length; i++) {
				((IModelStateListener) holdListeners[i]).modelAboutToBeChanged(this);
			}
		}

	}

	protected void fireModelAboutToBeReinitialized() {

		// we must assign listeners to local variable, since the add and
		// remove
		// listner
		// methods can change the actual instance of the listener array from
		// another thread
		if (fModelStateListeners != null) {
			if (Logger.DEBUG_MODELSTATE) {
				Logger.log(Logger.INFO, "IModelStateListener event for " + getId() + " : modelAboutToBeReinitialized"); //$NON-NLS-1$ //$NON-NLS-2$
			}
			Object[] holdListeners = fModelStateListeners;
			for (int i = 0; i < holdListeners.length; i++) {
				// NOTE: trick for transition. We actual use the same
				// listeners
				// as modelState, but only send this to those that have
				// implemented ModelStateExtended.
				IModelStateListener listener = (IModelStateListener) holdListeners[i];
				listener.modelAboutToBeReinitialized(this);
			}
		}
	}

	private void fireModelChanged() {
		// we must assign listeners
		// to local variable, since the add
		// and remove listner
		// methods can change the actual instance of the listener
		// array from another thread
		if (fModelStateListeners != null) {
			Object[] holdListeners = fModelStateListeners;
			for (int i = 0; i < holdListeners.length; i++) {
				try {
					((IModelStateListener) holdListeners[i]).modelChanged(this);
				}
				// its so criticial that the begin/end arrive in
				// pairs,
				// if there happends to be an error in one of the
				// modelChanged,
				// they we want to be sure rest complete ok.
				catch (Exception e) {
					Logger.logException(e);
				}
			}

		}
	}

	/**
	 * Informs all registered model state listeners about a change in the
	 * dirty state of the model. The dirty state is entirely about changes in
	 * the content of the model (not, for example, about changes to id, or
	 * base location -- see modelMoved).
	 */
	protected void fireModelDirtyStateChanged(IStructuredModel element, boolean isDirty) {

		// we must assign listeners to local variable, since the add and
		// remove
		// listner
		// methods can change the actual instance of the listener array from
		// another thread
		if (fModelStateListeners != null) {
			if (Logger.DEBUG_MODELSTATE) {
				Logger.log(Logger.INFO, "IModelStateListener event for " + getId() + " : modelDirtyStateChanged"); //$NON-NLS-1$ //$NON-NLS-2$
			}
			Object[] holdListeners = fModelStateListeners;
			for (int i = 0; i < holdListeners.length; i++) {
				((IModelStateListener) holdListeners[i]).modelDirtyStateChanged(element, isDirty);
			}
		}
	}

	protected void fireModelReinitialized() {

		// we must assign listeners to local variable, since the add and
		// remove
		// listner
		// methods can change the actual instance of the listener array from
		// another thread
		if (fModelStateListeners != null) {
			if (Logger.DEBUG_MODELSTATE) {
				Logger.log(Logger.INFO, "IModelStateListener event for " + getId() + " : modelReinitialized"); //$NON-NLS-1$ //$NON-NLS-2$
			}
			Object[] holdListeners = fModelStateListeners;
			for (int i = 0; i < holdListeners.length; i++) {
				IModelStateListener listener = (IModelStateListener) holdListeners[i];
				listener.modelReinitialized(this);
			}
		}
	}

	/**
	 * Informs all registered model state listeners about the deletion of a
	 * model's underlying resource.
	 */
	protected void fireModelResourceDeleted(IStructuredModel element) {

		// we must assign listeners to local variable, since the add and
		// remove
		// listner
		// methods can change the actual instance of the listener array from
		// another thread
		if (fModelStateListeners != null) {
			if (Logger.DEBUG_MODELSTATE) {
				Logger.log(Logger.INFO, "IModelStateListener event for " + getId() + " : modelResourceDeleted"); //$NON-NLS-1$ //$NON-NLS-2$
			}
			Object[] holdListeners = fModelStateListeners;
			for (int i = 0; i < holdListeners.length; i++) {
				((IModelStateListener) holdListeners[i]).modelResourceDeleted(element);
			}
		}
	}

	/**
	 * Informs all registered model state listeners that the resource
	 * underlying a model has been moved. This is typically reflected in a
	 * change to the id, baseLocation, or both.
	 */
	protected void fireModelResourceMoved(IStructuredModel originalElement, IStructuredModel movedElement) {

		// we must assign listeners to local variable, since the add and
		// remove
		// listner
		// methods can change the actual instance of the listener array from
		// another thread
		if (fModelStateListeners != null) {
			if (Logger.DEBUG_MODELSTATE) {
				Logger.log(Logger.INFO, "IModelStateListener event for " + getId() + " : modelResourceMoved"); //$NON-NLS-1$ //$NON-NLS-2$
			}
			Object[] holdListeners = fModelStateListeners;
			for (int i = 0; i < holdListeners.length; i++) {
				((IModelStateListener) holdListeners[i]).modelResourceMoved(originalElement, movedElement);
			}
		}
	}

	public Object getAdapter(Class adapter) {

		return Platform.getAdapterManager().getAdapter(this, adapter);
	}

	/**
	 * @return java.lang.String
	 */
	public java.lang.String getBaseLocation() {

		return fBaseLocation;
	}

	/**
	 * @see org.eclipse.wst.sse.core.internal.provisional.IStructuredModel#getContentTypeIdentifier()
	 */
	public String getContentTypeIdentifier() {
		if (fExplicitContentTypeIdentifier != null)
			return fExplicitContentTypeIdentifier;
		return fModelHandler.getAssociatedContentTypeId();
	}

	/**
	 * 
	 */
	public FactoryRegistry getFactoryRegistry() {
		if (factoryRegistry == null) {
			factoryRegistry = new FactoryRegistry();
		}
		return factoryRegistry;
	}

	/**
	 * The id is the id that the model manager uses to identify this model
	 * 
	 * @ISSUE - no one should need to know ID, so this should be default access eventually. 
	 * If clients believe they do need ID, be sure to let us know (open a bug). 
	 */
	public String getId() {

		return fId;
	}

	public abstract IndexedRegion getIndexedRegion(int offset);

	/**
	 * Gets the contentTypeDescription.
	 * 
	 * @return Returns a ContentTypeDescription
	 */
	public IModelHandler getModelHandler() {

		return fModelHandler;
	}


	public IModelManager getModelManager() {

		return _getModelManager();
	}

	/**
	 * This function returns the reference count of underlying model.
	 */
	// TODO: try to refine the design not to use this function
	public int getReferenceCount() {


		if (getModelManager() == null)
			return 0;
		return getModelManager().getReferenceCount(getId());
	}


	/**
	 * This function returns the reference count of underlying model.
	 */
	// TODO: try to refine the design not to use this function
	public int getReferenceCountForEdit() {



		if (getModelManager() == null)
			return 0;
		return getModelManager().getReferenceCountForEdit(getId());
	}


	/**
	 * This function returns the reference count of underlying model.
	 */
	// TODO: try to refine the design not to use this function
	public int getReferenceCountForRead() {



		if (getModelManager() == null)
			return 0;
		return getModelManager().getReferenceCountForRead(getId());
	}

	public Object getReinitializeStateData() {

		return reinitializeStateData;
	}



	public URIResolver getResolver() {

		return fResolver;
	}


	public IStructuredDocument getStructuredDocument() {

		IStructuredDocument result = null;
		result = fStructuredDocument;
		return result;
	}

	/**
	 * Insert the method's description here. Creation date: (9/7/2001 2:30:26
	 * PM)
	 * 
	 * @return long
	 */
	public long getSynchronizationStamp() {

		return fSynchronizationStamp;
	}

	public IStructuredTextUndoManager getUndoManager() {

		IStructuredTextUndoManager structuredTextUndoManager = null;
		IStructuredDocument structuredDocument = getStructuredDocument();
		if (structuredDocument == null) {
			structuredTextUndoManager = null;
		}
		else {
			structuredTextUndoManager = structuredDocument.getUndoManager();
		}
		return structuredTextUndoManager;
	}

	public void initId(String id) {
		fId = id;
	}

	private final Object STATE_LOCK = new Object();

	final protected void internalAboutToBeChanged() {
		int state = 0;
		// we always increment counter, for every request (so *must* receive
		// corresponding number of 'changedModel' requests)
		synchronized (STATE_LOCK) {
			state = fModelStateChanging++;
		}

		// notice we only fire this event if we are not
		// already in a model state changing sequence
		if (state == 0) {

			if (Logger.DEBUG_MODELSTATE) {
				Logger.log(Logger.INFO, "IModelStateListener event for " + getId() + " : modelAboutToBeChanged"); //$NON-NLS-1$ //$NON-NLS-2$
			}

			try {
				fireModelAboutToBeChanged();
			}
			catch (Exception e) {
				Logger.logException("Exception while notifying model state listers of about to change", e); //$NON-NLS-1$
			}

		}
		
	}

	/**
	 * Informs all registered model state listeners that an impending change
	 * is now complete. This method must only be called by 'modelChanged'
	 * since it keeps track of counts.
	 */
	final protected void internalModelChanged() {
		int state = 0;
		// always decrement
		synchronized (STATE_LOCK) {
			state = --fModelStateChanging;
		}

		// Check integrity
		// to be less than zero is a programming error,
		// but we'll reset to zero
		// and try to continue
		if (state < 0) {
			state = 0;
			throw new IllegalStateException("Program Error: modelStateChanging was less than zero"); //$NON-NLS-1$
		}


		// We only fire this event if all pending requests are done.
		// That is, if we've received the same number of modelChanged as
		// we have aboutToChangeModel.
		if (state == 0) {
			if (Logger.DEBUG_MODELSTATE) {
				Logger.log(Logger.INFO, "IModelStateListener event for " + getId() + " : modelChanged"); //$NON-NLS-1$ //$NON-NLS-2$
			}

			fireModelChanged();
		}
	}

	public boolean isDirty() {

		return fDirtyState;
	}

	/**
	 * This method has very special purpose, its used in subclass
	 * 'changedModel' to know when to do "ending" sorts of things, right
	 * before a call to super.ChangedModel would in deed put the model in
	 * 'end' state. Put another way, at the beginning of the subclasses's
	 * changedModel, the isModelStateChanging is true, but at end, it will be
	 * false. So, this method allows a small "peek ahead".
	 */
	protected boolean isModelChangeStateOnVergeOfEnding() {


		return fModelStateChanging == 1;
	}

	/**
	 * This method can be called to determine if the model is within a
	 * "aboutToChange" and "changed" sequence.
	 */
	public boolean isModelStateChanging() {


		return fModelStateChanging > 0;
	}

	public boolean isNew() {

		return fNewState;
	}

	public boolean isReinitializationNeeded() {

		return reinitializationNeeded;
	}

	public boolean isSaveNeeded() {


		if (!isSharedForEdit())
			return isDirty();
		else
			return false;
	}


	/**
	 * This function returns true if there are other references to the
	 * underlying model.
	 */
	public boolean isShared() {
		if (getModelManager() == null)
			return false;
		return getModelManager().isShared(getId());
	}


	/**
	 * This function returns true if there are other references to the
	 * underlying model.
	 */
	public boolean isSharedForEdit() {


		if (getModelManager() == null)
			return false;
		return getModelManager().isSharedForEdit(getId());
	}


	/**
	 * This function returns true if there are other references to the
	 * underlying model.
	 */
	public boolean isSharedForRead() {


		if (getModelManager() == null)
			return false;
		return getModelManager().isSharedForRead(getId());
	}


	public void modelReinitialized() {


		// notice this is just a public avenue to our protected method
		fireModelReinitialized();
	}

	public IStructuredModel newInstance() throws IOException {

		IStructuredModel newModel = null;
		// we delegate to the model manager, so loader, etc., can be
		// used.
		newModel = getModelManager().createNewInstance(this);
		return newModel;
	}

	public IStructuredModel reinit() {


		IStructuredModel result = null;
		if (fModelStateChanging == 0) {
			try {
				aboutToChangeModel();
				aboutToReinitializeModel();
				result = _getModelManager().reinitialize(this);
			}
			finally {
				setReinitializeNeeded(false);
				setReinitializeStateData(null);
				modelReinitialized();
				changedModel();
			}
		}
		else {
			if (Logger.DEBUG_MODELSTATE) {
				Logger.log(Logger.INFO, "indeed!!!"); //$NON-NLS-1$
			}
		}
		return result;
	}


	/**
	 * This function allows the model to free up any resources it might be
	 * using. In particular, itself, as stored in the IModelManager.
	 */
	public void releaseFromEdit() {


		if (getModelManager() == null) {
			throw new IllegalStateException(MODEL_MANAGER_NULL); //$NON-NLS-1$
		}
		else {
			/*
			 * Be sure to check the shared state before releasing. (Since
			 * isShared assumes a count of 1 means not shared ... and we want
			 * our '1' to be that one.) The problem, of course, is that
			 * between pre-cycle notification and post-release notification,
			 * the model could once again have become shared, rendering the
			 * release notification incorrect.
			 */
			boolean isShared = isShared();

			if (!isShared) {
				signalPreLifeCycleEventRelease(this);
			}

			_getModelManager().releaseFromEdit(this);
			if (!isShared) {
				signalPostLifeCycleListenerRelease(this);
			}
		}

	}

	/**
	 * This function allows the model to free up any resources it might be
	 * using. In particular, itself, as stored in the IModelManager.
	 */
	public void releaseFromRead() {

		if (getModelManager() == null) {
			throw new IllegalStateException(MODEL_MANAGER_NULL); //$NON-NLS-1$
		}
		else {
			/*
			 * Be sure to check the shared state before releasing. (Since
			 * isShared assumes a count of 1 means not shared ... and we want
			 * our '1' to be that one.) The problem, of course, is that
			 * between pre-cycle notification and post-release notification,
			 * the model could once again have become shared, rendering the
			 * release notification incorrect.
			 */
			boolean isShared = isShared();

			if (!isShared) {
				signalPreLifeCycleEventRelease(this);
			}

			_getModelManager().releaseFromRead(this);

			if (!isShared) {
				signalPostLifeCycleListenerRelease(this);
			}
		}
	}


	/**
	 * This function replenishes the model with the resource without saving
	 * any possible changes. It is used when one editor may be closing, and
	 * specifially says not to save the model, but another "display" of the
	 * model still needs to hang on to some model, so needs a fresh copy.
	 */
	public IStructuredModel reload(InputStream inputStream) throws IOException {
		IStructuredModel result = null;
		try {
			aboutToChangeModel();
			result = _getModelManager().reloadModel(getId(), inputStream);
		}
		catch (UnsupportedEncodingException e) {
			// log for now, unless we find reason not to
			Logger.log(Logger.INFO, e.getMessage());
		}
		finally {
			changedModel();
		}
		return result;
	}

	public void removeModelLifecycleListener(IModelLifecycleListener listener) {

		// if manager is null, then none have been added, so
		// no need to remove any
		if (fLifecycleNotificationManager == null)
			return;
		synchronized (fListenerLock) {
			fLifecycleNotificationManager.removeListener(listener);
		}
	}


	public void removeModelStateListener(IModelStateListener listener) {

		if (listener == null)
			return;
		if (fModelStateListeners == null)
			return;
		// if its not in the listeners, we'll ignore the request
		synchronized (fListenerLock) {
			if (Utilities.contains(fModelStateListeners, listener)) {
				int oldSize = fModelStateListeners.length;
				int newSize = oldSize - 1;
				Object[] newListeners = new Object[newSize];
				int index = 0;
				for (int i = 0; i < oldSize; i++) {
					if (fModelStateListeners[i] == listener) { // ignore
					}
					else {
						// copy old to new if its not the one we are
						// removing
						newListeners[index++] = fModelStateListeners[i];
					}
				}
				// now that we have a new array, let's switch it for the
				// old
				// one
				fModelStateListeners = newListeners;
			}
		}
	}


	/**
	 * A method that modifies the model's synchronization stamp to match the
	 * resource. Turns out there's several ways of doing it, so this ensures a
	 * common algorithm.
	 */
	public void resetSynchronizationStamp(IResource resource) {


		setSynchronizationStamp(computeModificationStamp(resource));
	}


	/**
	 * This API allows a client to initiate notification to all interested
	 * parties that a model's underlying resource has been deleted.
	 */
	public void resourceDeleted() {


		// notice this is just a public avenue to our protected method
		fireModelResourceDeleted(this);
	}


	/**
	 * This method allows a model client to initiate notification to all
	 * interested parties that a model's underlying resource location has
	 * changed. Note: we assume caller has already changed baseLocation, Id,
	 * etc., since its really up to the client to determine what's "new" about
	 * a moved model. Caution: 'this' and 'newModel' may be the same object.
	 * This is the case for current working with FileModelProvider, but have
	 * left the dual argument for future possibilities.
	 */
	public void resourceMoved(IStructuredModel newModel) {


		// notice this is just a public avenue to our protected method
		fireModelResourceMoved(this, newModel);
	}


	public void save() throws UnsupportedEncodingException, IOException, CoreException {

		int type = ModelLifecycleEvent.MODEL_SAVED | ModelLifecycleEvent.PRE_EVENT;
		ModelLifecycleEvent modelLifecycleEvent = new ModelLifecycleEvent(this, type);
		signalLifecycleEvent(modelLifecycleEvent);

		try {
			String stringId = getId();
			_getModelManager().saveModel(stringId, EncodingRule.CONTENT_BASED);
		}

		finally {
			// we put end notification in finally block, so even if
			// error occurs during save, listeners are still notified,
			// since their code could depend on receiving, to clean up
			// some state, or coordinate other resources.
			type = ModelLifecycleEvent.MODEL_SAVED | ModelLifecycleEvent.POST_EVENT;
			modelLifecycleEvent = new ModelLifecycleEvent(this, type);
			signalLifecycleEvent(modelLifecycleEvent);
		}
	}


	public void save(EncodingRule encodingRule) throws UnsupportedEncodingException, IOException, CoreException {

		int type = ModelLifecycleEvent.MODEL_SAVED | ModelLifecycleEvent.PRE_EVENT;
		ModelLifecycleEvent modelLifecycleEvent = new ModelLifecycleEvent(this, type);
		signalLifecycleEvent(modelLifecycleEvent);

		try {
			String stringId = getId();
			_getModelManager().saveModel(stringId, encodingRule);
		}
		finally {
			// we put end notification in finally block, so even if
			// error occurs during save, listeners are still notified,
			// since their code could depend on receiving, to clean up
			// some state, or coordinate other resources.
			type = ModelLifecycleEvent.MODEL_SAVED | ModelLifecycleEvent.POST_EVENT;
			modelLifecycleEvent = new ModelLifecycleEvent(this, type);
			signalLifecycleEvent(modelLifecycleEvent);
		}
	}


	public void save(IFile iFile) throws UnsupportedEncodingException, IOException, CoreException {

		int type = ModelLifecycleEvent.MODEL_SAVED | ModelLifecycleEvent.PRE_EVENT;
		ModelLifecycleEvent modelLifecycleEvent = new ModelLifecycleEvent(this, type);
		signalLifecycleEvent(modelLifecycleEvent);

		try {
			String stringId = getId();
			_getModelManager().saveModel(iFile, stringId, EncodingRule.CONTENT_BASED);
		}

		finally {
			// we put end notification in finally block, so even if
			// error occurs during save, listeners are still notified,
			// since their code could depend on receiving, to clean up
			// some state, or coordinate other resources.
			type = ModelLifecycleEvent.MODEL_SAVED | ModelLifecycleEvent.POST_EVENT;
			modelLifecycleEvent = new ModelLifecycleEvent(this, type);
			signalLifecycleEvent(modelLifecycleEvent);
		}
	}


	public void save(IFile iFile, EncodingRule encodingRule) throws UnsupportedEncodingException, IOException, CoreException {

		int type = ModelLifecycleEvent.MODEL_SAVED | ModelLifecycleEvent.PRE_EVENT;
		ModelLifecycleEvent modelLifecycleEvent = new ModelLifecycleEvent(this, type);
		signalLifecycleEvent(modelLifecycleEvent);

		try {
			String stringId = getId();
			_getModelManager().saveModel(iFile, stringId, encodingRule);
		}
		finally {
			// we put end notificatioon in finally block, so even if
			// error occurs during save, listeners are still notified,
			// since their code could depend on receiving, to clean up
			// some state, or coordinate other resources.
			type = ModelLifecycleEvent.MODEL_SAVED | ModelLifecycleEvent.POST_EVENT;
			modelLifecycleEvent = new ModelLifecycleEvent(this, type);
			signalLifecycleEvent(modelLifecycleEvent);
		}
	}


	public void save(OutputStream outputStream) throws UnsupportedEncodingException, CoreException, IOException {

		int type = ModelLifecycleEvent.MODEL_SAVED | ModelLifecycleEvent.PRE_EVENT;
		ModelLifecycleEvent modelLifecycleEvent = new ModelLifecycleEvent(this, type);
		signalLifecycleEvent(modelLifecycleEvent);

		try {
			String stringId = getId();
			_getModelManager().saveModel(stringId, outputStream, EncodingRule.CONTENT_BASED);
		}

		finally {
			// we put end notification in finally block, so even if
			// error occurs during save, listeners are still notified,
			// since their code could depend on receiving, to clean up
			// some state, or coordinate other resources.
			type = ModelLifecycleEvent.MODEL_SAVED | ModelLifecycleEvent.POST_EVENT;
			modelLifecycleEvent = new ModelLifecycleEvent(this, type);
			signalLifecycleEvent(modelLifecycleEvent);
		}
	}


	/**
	 * This attribute is typically used to denote the model's underlying
	 * resource.
	 */
	public void setBaseLocation(java.lang.String newBaseLocation) {
		fBaseLocation = newBaseLocation;
		if (fResolver != null) {
			fResolver.setFileBaseLocation(newBaseLocation);
		}
	}

	public void setContentTypeIdentifier(String contentTypeIdentifier) {
		fExplicitContentTypeIdentifier = contentTypeIdentifier;
	}

	/**
	 * 
	 */
	public void setDirtyState(boolean dirtyState) {

		// no need to process (set or fire event), if same value
		if (fDirtyState != dirtyState) {
			// pre-change notification
			int type = ModelLifecycleEvent.MODEL_DIRTY_STATE | ModelLifecycleEvent.PRE_EVENT;
			ModelLifecycleEvent modelLifecycleEvent = new ModelLifecycleEvent(this, type);
			signalLifecycleEvent(modelLifecycleEvent);


			// the actual change
			fDirtyState = dirtyState;

			// old notification
			// TODO: C3 remove old notification
			if (fDirtyState == false) {
				// if we are being set to not dirty (such as just been saved)
				// then we need to start listening for changes
				// again to know when to set state to true;
				getStructuredDocument().addDocumentChangedListener(fDirtyStateWatcher);
			}
			fireModelDirtyStateChanged(this, dirtyState);


			// post change notification
			type = ModelLifecycleEvent.MODEL_DIRTY_STATE | ModelLifecycleEvent.POST_EVENT;
			modelLifecycleEvent = new ModelLifecycleEvent(this, type);
			signalLifecycleEvent(modelLifecycleEvent);
		}
	}

	/**
	 * @deprecated - will likely be deprecated soon, in favor of direct 'adds'
	 *             ... but takes some redesign.
	 */
	public void setFactoryRegistry(FactoryRegistry factoryRegistry) {
		this.factoryRegistry = factoryRegistry;
	}

	/**
	 * The id is the id that the model manager uses to identify this model. If
	 * it is being set here, it means the model manger is already managing the
	 * model with another id, so we have to keep it in sync. This method calls
	 * notifies listners, if they haven't been notified already, that a "model
	 * state change" is about to occur.
	 */
	public void setId(String newId) throws ResourceInUse {


		// It makes no sense, I don't think, to have an id of null, so
		// we'll throw an illegal argument exception if someone trys. Note:
		// the IModelManager could not manage a model with an id of null,
		// since it uses hashtables, and you can't have a null id for a
		// hashtable.
		if (newId == null)
			throw new IllegalArgumentException(SSECoreMessages.A_model_s_id_can_not_be_nu_EXC_); //$NON-NLS-1$ = "A model's id can not be null"
		// To guard against throwing a spurious ResourceInUse exception,
		// which can occur when two pieces of code both want to change the id,
		// so the second request is spurious, we'll ignore any requests that
		// attempt to change the id to what it already is ... note, we use
		// 'equals', not identity ('==') so that things like
		// strings can be used. This is the same criteria that ids are
		// found in model manager -- well, actually, I just checked, and for
		// the hashtable impl, the criteria uses .equals AND the condition
		// that the hash values be identical (I'm assuming this is always
		// true, if equals is true, for now, I'm not sure
		// we can assume that hashtable will always be used, but in
		// general, should match.)
		//
		if (newId.equals(fId))
			return;
		// we must guard against reassigning an id to one that we already
		// are managing.
		if (getModelManager() != null) {
			boolean inUse = ((ModelManagerImpl)getModelManager()).isIdInUse(newId);
			if (inUse) {
				throw new ResourceInUse();
			}
		}
		try {
			// normal code path
			aboutToChangeModel();
			String oldId = fId;
			fId = newId;
			if (getModelManager() != null) {
				// if managed and the id has changed, notify to
				// IModelManager
				// TODO: try to refine the design not to do that
				if (oldId != null && newId != null && !newId.equals(oldId)) {
					getModelManager().moveModel(oldId, newId);
				}
			}
		}
		finally {
			// make sure this finally is only executed if 'about to Change
			// model' has
			// been executed.
			changedModel();
		}
	}

	/**
	 * Sets the contentTypeDescription.
	 * 
	 * @param contentTypeDescription
	 *            The contentTypeDescription to set
	 */
	public void setModelHandler(IModelHandler modelHandler) {

		// no need to fire events if modelHandler has been null
		// for this model --
		// this is an attempt at initialization optimization and may need
		// to change in future.
		boolean trueChange = false;
		if (fModelHandler != null)
			trueChange = true;
		if (trueChange) {
			internalAboutToBeChanged();
		}
		fModelHandler = modelHandler;
		if (trueChange) {
			internalModelChanged();
		}
	}



	public void setModelManager(IModelManager newModelManager) {

		fModelManager = newModelManager;
	}

	/**
	 * 
	 */
	public void setNewState(boolean newState) {

		fNewState = newState;
	}

	/**
	 * Sets a "flag" that reinitialization is needed.
	 */
	public void setReinitializeNeeded(boolean needed) {

		reinitializationNeeded = needed;
	}

	/**
	 * Holds any data that the reinit procedure might find useful in
	 * reinitializing the model. This is handy, since the reinitialization may
	 * not take place at once, and some "old" data may be needed to properly
	 * undo previous settings. Note: the parameter was intentionally made to
	 * be of type 'Object' so different models can use in different ways.
	 */
	public void setReinitializeStateData(Object object) {

		reinitializeStateData = object;
	}


	public void setResolver(URIResolver newResolver) {

		fResolver = newResolver;
	}


	public void setStructuredDocument(IStructuredDocument newStructuredDocument) {
		boolean lifeCycleNotification = false;
		if (fStructuredDocument != null) {
			fStructuredDocument.removeDocumentChangedListener(fDirtyStateWatcher);
			fStructuredDocument.removeDocumentAboutToChangeListener(fDocumentToModelNotifier);
			fStructuredDocument.removeDocumentChangedListener(fDocumentToModelNotifier);
			// prechange notification
			lifeCycleNotification = true;
			ModelLifecycleEvent modelLifecycleEvent = new DocumentChanged(ModelLifecycleEvent.PRE_EVENT, this, fStructuredDocument, newStructuredDocument);
			signalLifecycleEvent(modelLifecycleEvent);
		}

		// hold for life cycle notification
		IStructuredDocument previousDocument = fStructuredDocument;
		// the actual change
		fStructuredDocument = newStructuredDocument;


		// at the super class level, we'll listen for structuredDocument
		// changes
		// so we can set our dirty state flag
		if (fStructuredDocument != null) {
			fStructuredDocument.addDocumentChangedListener(fDirtyStateWatcher);
			fStructuredDocument.addDocumentAboutToChangeListener(fDocumentToModelNotifier);
			fStructuredDocument.addDocumentChangedListener(fDocumentToModelNotifier);
		}

		if (lifeCycleNotification) {
			// post change notification
			ModelLifecycleEvent modelLifecycleEvent = new DocumentChanged(ModelLifecycleEvent.POST_EVENT, this, previousDocument, newStructuredDocument);
			signalLifecycleEvent(modelLifecycleEvent);
		}
	}

	/**
	 * Insert the method's description here. Creation date: (9/7/2001 2:30:26
	 * PM)
	 * 
	 * @param newSynchronizationStamp
	 *            long
	 */
	protected void setSynchronizationStamp(long newSynchronizationStamp) {

		fSynchronizationStamp = newSynchronizationStamp;
	}

	public void setUndoManager(IStructuredTextUndoManager undoManager) {

		IStructuredDocument structuredDocument = getStructuredDocument();
		if (structuredDocument == null) {
			throw new IllegalStateException("document was null when undo manager set on model"); //$NON-NLS-1$
		}
		structuredDocument.setUndoManager(undoManager);
	}

	/**
	 * To be called only by "friendly" classes, such as ModelManager, and
	 * subclasses.
	 */
	void signalLifecycleEvent(ModelLifecycleEvent event) {
		if (fLifecycleNotificationManager == null)
			return;
		fLifecycleNotificationManager.signalLifecycleEvent(event);
	}

	private void signalPostLifeCycleListenerRelease(IStructuredModel structuredModel) {
		int type = ModelLifecycleEvent.MODEL_RELEASED | ModelLifecycleEvent.POST_EVENT;
		// what's wrong with this design that a cast is needed here!?
		ModelLifecycleEvent event = new ModelLifecycleEvent(structuredModel, type);
		((AbstractStructuredModel) structuredModel).signalLifecycleEvent(event);
	}

	private void signalPreLifeCycleEventRelease(IStructuredModel structuredModel) {
		int type = ModelLifecycleEvent.MODEL_RELEASED | ModelLifecycleEvent.PRE_EVENT;
		// what's wrong with this design that a cast is needed here!?
		ModelLifecycleEvent event = new ModelLifecycleEvent(structuredModel, type);
		((AbstractStructuredModel) structuredModel).signalLifecycleEvent(event);
	}
}
