/*******************************************************************************
 * Copyright (c) 2001, 2010 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
 *******************************************************************************/

package org.eclipse.wst.xml.core.internal.emf2xml;


import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.eclipse.core.resources.IContainer;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IFolder;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecore.resource.Resource;
import org.eclipse.emf.ecore.resource.ResourceSet;
import org.eclipse.emf.ecore.util.EcoreUtil;
import org.eclipse.jem.util.emf.workbench.ProjectResourceSet;
import org.eclipse.jem.util.emf.workbench.WorkbenchResourceHelperBase;
import org.eclipse.wst.common.internal.emf.resource.EMF2DOMAdapter;
import org.eclipse.wst.common.internal.emf.resource.EMF2DOMRenderer;
import org.eclipse.wst.common.internal.emf.resource.TranslatorResource;
import org.eclipse.wst.common.internal.emf.utilities.DOMUtilities;
import org.eclipse.wst.common.internal.emfworkbench.WorkbenchResourceHelper;
import org.eclipse.wst.common.internal.emfworkbench.integration.ResourceSetWorkbenchEditSynchronizer;
import org.eclipse.wst.sse.core.StructuredModelManager;
import org.eclipse.wst.sse.core.internal.model.ModelLifecycleEvent;
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.INodeAdapter;
import org.eclipse.wst.sse.core.internal.provisional.INodeNotifier;
import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel;
import org.eclipse.wst.xml.core.internal.Logger;
import org.eclipse.wst.xml.core.internal.document.DocumentTypeImpl;
import org.eclipse.wst.xml.core.internal.provisional.document.IDOMModel;
import org.eclipse.wst.xml.core.internal.provisional.document.IDOMNode;
import org.w3c.dom.Node;


public class EMF2DOMSSERenderer extends EMF2DOMRenderer implements IModelStateListener, IModelLifecycleListener {

	protected Object aboutToChangeNode = null;

	protected boolean isBatchChanges = false;

	private boolean isSaving = false;

	private IModelManager modelManager;
	
	private Class resourceClass;
	
	private boolean reverting = false;

	/** The XML DOM model */
	protected IDOMModel xmlModel;

	/** Used internally; the unique id for the xml model */
	protected String xmlModelId;

	protected boolean xmlModelReverted = false;

	public EMF2DOMSSERenderer() {
		super();
	}

	public void accessForRead() {
		if (!resource.isNew()) {
			String id = getModelManagerId();
			getModelManager().getExistingModelForRead(id);
		}
	}

	public void accessForWrite() {
		modelAccessForWrite();
	}

	/**
	 * see batchModelEnd
	 * 
	 */

	public void batchModeEnd() {
		getXMLModel().changedModel();
		setRootNodeAdapterNotificationEnabled(true);
		isBatchChanges = false;
	}

	/**
	 * batchModeStart and batchModeEnd is a pair that controls notifications,
	 * and tread access. They should always be called in a try/finally block.
	 * 
	 * setBatchModel begins the processing where notifications are not sent
	 * out on each change, but saved up until the endBatchMode called.
	 * 
	 * This pair of calls can also, indirectly, "lock" the DOM Model to access
	 * from only one thread, so it should not be locked for long periods of
	 * time. That's also why it is important to have the endBatchMode in a
	 * finally block to be sure it is always called, or the DOM will be left
	 * in a locked, unusable, state and only shortly away from severere
	 * program error.
	 * 
	 * <pre><code>
	 * Example
	 * </code>
	 *  
	 *  try { 
	 *  		batchModelStart();
	 *  		...do a some work ...
	 *  		}
	 *  	finally {
	 *  		endBatchMode();
	 *  		}
	 *  
	 *  
	 * </pre>
	 */
	public void batchModeStart() {
		isBatchChanges = true;
		getXMLModel().aboutToChangeModel();
		setRootNodeAdapterNotificationEnabled(false);
	}

	private void cacheSynchronizationStamp() {
		IFile file = WorkbenchResourceHelper.getFile(resource);
		if (file != null) {
			if (xmlModel != null)
				xmlModel.resetSynchronizationStamp(file);
		}
	}

	/**
	 * Create a new Document given
	 * 
	 * @aResource.
	 */
	protected void createDocument() {
		TranslatorResource res = getResource();
		res.setDefaults();
		IFile file = WorkbenchResourceHelper.getFile(resource);
		InputStream is = DOMUtilities.createHeaderInputStream(res.getDoctype(), res.getPublicId(), res.getSystemId());
		if (is == null)
			return;
		try {
			try {
				List folders = new ArrayList();
				IContainer container = file.getParent();
				while (null != container && !container.exists() && container instanceof IFolder) {
					folders.add(container);
					container = container.getParent();
				}
				IFolder folder = null;
				for (int i = 0; i < folders.size(); i++) {
					folder = (IFolder) folders.get(i);
					folder.create(true, true, null);
				}
				file.create(is, true, null);
				file.setLocal(true, 1, null);
			}
			catch (CoreException e1) {
				Logger.logException(e1);
			}
			finally {
				if (null != is) {
					is.close();
				}
			}
			initializeXMLModel(file, resource.getWriteCount() != 0);
		}
		catch (IOException ex) {
			Logger.log(Logger.ERROR, "IWAE0017E Unexpected IO exception occurred creating xml document");//$NON-NLS-1$
		}
	}

	protected void createDOMTreeIfNecessary() {
		if (needsToCreateDOM)
			createDOMTree();
	}

	protected EMF2DOMAdapter createRootDOMAdapter() {
		return new EMF2DOMSSEAdapter(getResource(), document, this, getResource().getRootTranslator());
	}

	public void deRegisterAsModelLifecycleListener() {
		if (xmlModel != null)
			xmlModel.removeModelLifecycleListener(this);
	}

	public void deRegisterAsModelStateListener() {
		if (xmlModel != null)
			xmlModel.removeModelStateListener(this);
	}

	private void deregisterFromXMLModel() {
		
		deRegisterAsModelStateListener();
		deRegisterAsModelLifecycleListener();
		// This try/catch block is a hack to fix defect 204114. This occurs
		// because
		// the model manager plugin is shut down and unloaded before the j2ee
		// plugin.
		// Calling getModelManager() can result in a class cast exception that
		// should
		// be ignored.
		// ModelManager mgr = null;
		try {
			getModelManager();
		}
		catch (ClassCastException exc) {
			return;
		}
		if (xmlModel != null) {
			int writeCount = resource.getWriteCount();
			int readCount = resource.getReadCount();
			for (int i = 0; i < writeCount; i++)
				xmlModel.releaseFromEdit();
			for (int ii = 0; ii < readCount; ii++)
				xmlModel.releaseFromRead();
		}
		EMF2DOMAdapter adapter = (EMF2DOMAdapter) EcoreUtil.getAdapter(resource.eAdapters(), EMF2DOMAdapter.ADAPTER_CLASS);
		if (adapter != null) {
			adapter.removeAdapters(adapter.getNode());
		}
		xmlModel = null;
		xmlModelId = null;
	}

	public void doSave(OutputStream outputStream, Map options) throws IOException {

		try {
			isSaving = true;
			if (null != outputStream) {
				throw new RuntimeException(this.getClass().getName() + " should not use an OutputStream for saving");} //$NON-NLS-1$
			createDOMTreeIfNecessary();
			ResourceSetWorkbenchEditSynchronizer synchronizer = (ResourceSetWorkbenchEditSynchronizer) ((ProjectResourceSet) resource.getResourceSet()).getSynchronizer();
			IFile aFile = WorkbenchResourceHelper.getFile(resource);
			try {
				synchronizer.preSave(aFile, resource);
				xmlModel.save(aFile);
			}
			catch (CoreException ex) {
				synchronizer.removeFromRecentlySavedList(aFile);
				Logger.logException(ex);
			}
			cacheSynchronizationStamp();
		}
		finally {
			isSaving = false;
		}
	}

	public EMF2DOMAdapter getExistingDOMAdapter(Node node) {
		//IDOMNode xNode = (IDOMNode) node;
		//return (EMF2DOMSSEAdapter) xNode.getAdapterFor(EMF2DOMAdapter.ADAPTER_CLASS);

		INodeNotifier sseNode = (INodeNotifier) node;
		Collection adapters = sseNode.getAdapters();
		for (Iterator iterator = adapters.iterator(); iterator.hasNext();) {
			INodeAdapter adapter = (INodeAdapter) iterator.next();
			// First Check if it's an EMF2DOMAdapter
			if (adapter != null && adapter.isAdapterForType(EMF2DOMAdapter.ADAPTER_CLASS)) {
				// Cast to EMF2DOMSSEAdapter
				EMF2DOMSSEAdapter e2DAdapter = (EMF2DOMSSEAdapter) adapter;
				//Handle the cases where either adapter's target is null 
				//Use the resourceClass to make sure the resource type is identical
				if (getResource() == null || e2DAdapter.getTarget() == null)
					if(resourceClass.equals(e2DAdapter.getResourceClass()))
						return e2DAdapter;
					else
						continue;
				
				// First check if targets are resources
				if (e2DAdapter.getTarget() instanceof Resource) {
					/*
					 * Now check if it's the right one (Multiple resources
					 * could be attached)
					 */
					if (e2DAdapter.getTarget() == getResource()) {
						return e2DAdapter;
					}
				} else {
					// Check if targets are EObjects with the same resources
					TranslatorResource myTarget = getResource();
					EObject adapterTarget = (EObject) e2DAdapter.getTarget();
					/*
					 * Now check if it's the right one (Multiple resources could
					 * be attached)
					 */
					if (adapterTarget.eResource() == myTarget || adapterTarget.eResource() == null) {
						return e2DAdapter;
					}
				}
			}
		}
		return null;
	
	}

	protected IModelManager getModelManager() {
		if (modelManager == null)
			modelManager = StructuredModelManager.getModelManager();
		return modelManager;
	}

	/**
	 * Return id used to key the XML resource in the XML ModelManager.
	 */
	protected String getModelManagerId() {
		if (xmlModelId == null) {
			IFile file = WorkbenchResourceHelper.getFile(getResource());
			if (file != null) {
				xmlModelId = getModelManager().calculateId(file);
			}
			else {
				xmlModelId = resource.getURI() + Long.toString(System.currentTimeMillis());
			}
		}
		return xmlModelId;
	}

	protected ResourceSet getResourceSet() {
		return resource == null ? null : resource.getResourceSet();
	}

	/**
	 * Return the DOM model for this resource.
	 */
	public IDOMModel getXMLModel() {
		return xmlModel;
	}

	public String getXMLModelId() {
		return xmlModelId;
	}	
	
	private IDOMModel initializeXMLModel(IFile file, boolean forWrite) throws UnsupportedEncodingException, IOException {
		if (file == null || !file.exists())
			throw new FileNotFoundException((file == null) ? "null" : file.getFullPath().toOSString()); //$NON-NLS-1$
		try {
			IModelManager manager = getModelManager();
			String id = manager.calculateId(file);			
			if (forWrite) {
				IDOMModel mod = (IDOMModel)manager.getExistingModelForEdit(id);
				if (mod == null)
					setXMLModel((IDOMModel) manager.getModelForEdit(file));
				else {
					setXMLModel(mod);
				}
			}
			else {
				IDOMModel mod = (IDOMModel)manager.getExistingModelForRead(id);
				if (mod == null)
					setXMLModel((IDOMModel) manager.getModelForRead(file));
				else {
					setXMLModel(mod);
				}
			}
			setXMLModelId(getXMLModel().getId());
			needsToCreateDOM = false;
		}
		catch (CoreException e) {
			Logger.logException(e);
			return null;
		}
		String id = getXMLModel().getId();
		syncReferenceCounts(id, forWrite);
		if (xmlModel != null)
			document = xmlModel.getDocument();
		return xmlModel;
	}

	public boolean isBatchMode() {
		return isBatchChanges;
	}

	public boolean isModified() {
		return (getXMLModel() != null && getXMLModel().isDirty());
	}

	public boolean isShared() {
		
		if( xmlModel == null) { //resource could be in process of being unloaded - check with model manager
			String id = getModelManagerId();
			return getModelManager().isShared(id);
		}
		if (getResourceSet() == null)
			return false;
		return xmlModel.isShared();
	}

	public boolean isSharedForWrite() {
		if (getResourceSet() == null || xmlModel == null)
			return false;
		return xmlModel.isSharedForEdit();
	}

	protected void loadDocument(InputStream in, Map options) throws IOException {
		if (null != in) {
			throw new RuntimeException(this.getClass().getName() + " should not use an InputStream for loading");} //$NON-NLS-1$
		IFile file = WorkbenchResourceHelper.getFile(resource);
		initializeXMLModel(file, (resource.getWriteCount() != 0));
		cacheSynchronizationStamp();
	}

	protected boolean managesDOMAdapters() {
		return false;
	}

	public void modelAboutToBeChanged(IStructuredModel model) {
		if (model.getStructuredDocument() != null)
			aboutToChangeNode = model.getStructuredDocument().getFirstStructuredDocumentRegion();
	}

	public void modelAboutToBeReinitialized(IStructuredModel structuredModel) {
		// TODO Auto-generated method stub

	}

	private void modelAccessForWrite() {
		String id = getModelManagerId();
		getModelManager().getExistingModelForEdit(id);
	}

	public void modelChanged(IStructuredModel model) {
		if (isBatchChanges)
			return;
		try {
			if (reverting && aboutToChangeNode != null && model.getStructuredDocument() != null
					&& model.getStructuredDocument().getFirstStructuredDocumentRegion() != aboutToChangeNode) {
				String id = getModelManagerId();
				IStructuredModel tempModel = null;
				try {
					tempModel = getModelManager().getExistingModelForEdit(id);
					xmlModelReverted = true;
					resource.unload();
				} finally {
					if (tempModel != null && (tempModel.getReferenceCountForEdit() > 0)) {
						tempModel.releaseFromEdit();
					}
					reverting = false;
				}
			}
		} finally {
			aboutToChangeNode = null;
		}
	} 
	
	public void modelDirtyStateChanged(IStructuredModel model, boolean isDirty) {
		if (!isDirty && resource.isModified()) { // The XMLModel was saved
			resource.setModified(false);
			long stamp = WorkbenchResourceHelper.computeModificationStamp(resource);
			WorkbenchResourceHelper.setSynhronizationStamp(resource, stamp);
			IProject proj = WorkbenchResourceHelper.getProject(resource);
			ResourceSetWorkbenchEditSynchronizer synchronizer = (ResourceSetWorkbenchEditSynchronizer) ((ProjectResourceSet) WorkbenchResourceHelperBase.getResourceSet(proj)).getSynchronizer();
			IFile aFile = WorkbenchResourceHelper.getFile(resource);
			synchronizer.preSave(aFile, resource);
		}
		if (isDirty)
			resource.setModified(true);
	}

	public void modelReinitialized(IStructuredModel structuredModel) {
		// TODO Auto-generated method stub

	}

	public void modelResourceDeleted(IStructuredModel model) {
		// Do nothing
	}

	public void modelResourceMoved(IStructuredModel oldModel, IStructuredModel newModel) {
		// Do nothing
	}

	/**
	 * This method is called just prior to being removed from the ResourceSet.
	 * Ensure that all reference counts to the XMLModel are removed.
	 */
	public void preDelete() {
		if (resource.isLoaded())
			deregisterFromXMLModel();
	}

	public void preUnload() {
		deregisterFromXMLModel();
	}

	public void processPostModelEvent(ModelLifecycleEvent event) {
		if (event.getType() == ModelLifecycleEvent.MODEL_REVERT)
			reverting = false;

	}

	public void processPreModelEvent(ModelLifecycleEvent event) {
		if (event.getType() == ModelLifecycleEvent.MODEL_REVERT)
			reverting = true;
			
		if (!isSaving) {
			if (event.getType() == ModelLifecycleEvent.MODEL_SAVED) {
				ResourceSetWorkbenchEditSynchronizer synchronizer = (ResourceSetWorkbenchEditSynchronizer) ((ProjectResourceSet) resource.getResourceSet()).getSynchronizer();
				IFile aFile = WorkbenchResourceHelper.getFile(resource);
				synchronizer.preSave(aFile, resource);
			}
		}
		
	}


	public void registerAsModelLifecycleListener() {
		this.xmlModel.addModelLifecycleListener(this);
	}

	/**
	 * Insert the method's description here. Creation date: (9/7/2001 10:49:53
	 * AM)
	 */
	public void registerAsModelStateListener() {
		this.xmlModel.addModelStateListener(this);
	}

	public void releaseFromRead() {
		if (xmlModel != null)
			xmlModel.releaseFromRead();
	}

	public void releaseFromWrite() {
		if (xmlModel != null)
			xmlModel.releaseFromEdit();
	}

	public void removeDOMAdapter(Node aNode, EMF2DOMAdapter anAdapter) {
		((IDOMNode) aNode).removeAdapter((EMF2DOMSSEAdapter) anAdapter);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.eclipse.jem.internal.util.emf.xml.EMF2DOMRenderer#replaceDocumentType(java.lang.String,
	 *      java.lang.String, java.lang.String)
	 */
	public void replaceDocumentType(String docTypeName, String publicId, String systemId) {
		if (document == null)
			return;
		DocumentTypeImpl docType = (DocumentTypeImpl) document.getDoctype();
		if (docType == null)
			return;
		if (publicId == null && systemId == null)
			document.removeChild(docType);
		else {
			docType.setPublicId(publicId);
			docType.setSystemId(systemId);
		}
	}

	/**
	 * @deprecated use batchModeStart and BatchModeEnd instead even if you do
	 *             not use batchModelStart/End, you still need to use the
	 *             try/finally pattern documented there.
	 */

	public void setBatchMode(boolean isBatch) {

		// This is some extra processing for clients to know they may be using
		// incorrectly
		if (isBatch) {
			if (isBatchChanges) {
				Logger.log(Logger.INFO_DEBUG, "setBatch was set to true when it was already true. This can be an indication of invalid calling order"); //$NON-NLS-1$
			}
		}


		if (isBatch) {
			batchModeStart();
		}
		else {
			batchModeEnd();
		}
	}

	private void setRootNodeAdapterNotificationEnabled(boolean b) {
		EObject root = resource.getRootObject();
		if (root != null) {
			EMF2DOMAdapter adapter = (EMF2DOMAdapter) EcoreUtil.getExistingAdapter(root, EMF2DOMAdapter.ADAPTER_CLASS);
			if (adapter != null) {
				adapter.setNotificationEnabled(b);
				if (b)
					adapter.updateDOM();
			}
		}
	}

	/**
	 * Return the DOM model for this resource.
	 */
	public void setXMLModel(IDOMModel xmlModel) {
		deRegisterAsModelStateListener();
		deRegisterAsModelLifecycleListener();
		this.xmlModel = xmlModel;
		registerAsModelStateListener();
		registerAsModelLifecycleListener();
	}

	public void setXMLModelId(String id) {
		xmlModelId = id;
	}

	private void syncReferenceCounts(String id, boolean forWrite) {
		int editIndex = 0, readIndex = 0;
		if (forWrite)
			editIndex++;
		else
			readIndex++;
		int writeCount = resource.getWriteCount();
		int readCount = resource.getReadCount();
		for (int i = writeCount; i > editIndex; i--)
			modelManager.getExistingModelForEdit(id);
		for (int i = readCount; i > readIndex; i--)
			modelManager.getExistingModelForRead(id);
	}

	public boolean useStreamsForIO() {
		return false;
	}

	public boolean wasReverted() {
		return xmlModelReverted;
	}

	public void setResource(TranslatorResource resource) {
		super.setResource(resource);
		if (resource != null)
			resourceClass = resource.getClass();
	}
	public boolean isReverting() {
		return this.reverting;
	}


}
