//------------------------------------------------------------------------------
// Copyright (c) 2005, 2006 IBM Corporation and others.
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// which accompanies this distribution, and is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// Contributors:
// IBM Corporation - initial implementation
//------------------------------------------------------------------------------
package org.eclipse.epf.persistence;

import java.util.Iterator;
import java.util.Map;
import java.util.Set;

import org.eclipse.emf.common.CommonPlugin;
import org.eclipse.emf.common.util.URI;
import org.eclipse.emf.ecore.EClass;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecore.EReference;
import org.eclipse.emf.ecore.EStructuralFeature;
import org.eclipse.emf.ecore.InternalEObject;
import org.eclipse.emf.ecore.resource.Resource;
import org.eclipse.emf.ecore.resource.ResourceSet;
import org.eclipse.emf.ecore.xmi.XMLHelper;
import org.eclipse.emf.ecore.xmi.XMLResource;
import org.eclipse.emf.ecore.xmi.impl.XMISaveImpl;
import org.eclipse.epf.common.serviceability.VersionUtil;
import org.eclipse.epf.resourcemanager.ResourceManager;
import org.eclipse.epf.uma.MethodElement;

/**
 * @author Phong Nguyen Le
 * @since 1.0
 */
public class MultiFileXMISaveImpl extends XMISaveImpl {

	public static final String SAVE_SEPARATELY_CLASS_SET = "SAVE_SEPARATELY_CLASS_SET"; //$NON-NLS-1$

	/**
	 * Save options to force saving all resources in the resource set, whether
	 * they have been modified or unchanged. Its value must be a string "true"
	 * or "false"
	 */
	public static final String SAVE_ALL = "SAVE_ALL"; //$NON-NLS-1$

	/**
	 * Save option to save objects of the save type together in the same file if
	 * MultiFileUtil.createFileURI() returns the same URI for them.
	 */
	public static final String SAVE_TOGETHER_CLASS_SET = "SAVE_TOGETHER_CLASS_SET"; //$NON-NLS-1$

	public static final String BACK_UP_BEFORE_SAVE = "BACK_UP_BEFORE_SAVE"; //$NON-NLS-1$

	public static final String DISCARD_UNRESOLVED_REFERENCES = "DISCARD_UNRESOLVED_REFERENCES"; //$NON-NLS-1$

	static final String MODIFIED_RESOURCE_SET = "MODIFIED_RESOURCE_SET"; //$NON-NLS-1$

	/**
	 * Save option to specify a TxRecord to log the transactional data for
	 * fail-safe persistence
	 * 
	 * @see org.eclipse.epf.uma.util.IMethodLibraryPersister.FailSafeMethodLibraryPersister
	 * @see TxRecord
	 */
	static final String TX_RECORD = "TX_RECORD"; //$NON-NLS-1$

	public static final String MULTI_FILE = "MULTI_FILE"; //$NON-NLS-1$

	/**
	 * Save option to refresh the workspace when new resource is created and
	 * saved. Its value must be a string "true" or "false"
	 */
	public static final String REFRESH_NEW_RESOURCE = "REFRESH_NEW_RESOURCE"; //$NON-NLS-1$

	/**
	 * Save option to check the resource for modifiable before saving it. Its
	 * value must be a string "true" or "false"
	 */
	public static final String CHECK_MODIFY = "CHECK_MODIFY"; //$NON-NLS-1$

	// private Resource resource;
	private Set saveSeparatelyClassSet;

	private Map options;

	/**
	 * @param helper
	 */
	public MultiFileXMISaveImpl(XMLHelper helper) {
		super(helper);
	}

	XMLHelper getXMLHelper() {
		return helper;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.eclipse.emf.ecore.xmi.impl.XMLSaveImpl#init(org.eclipse.emf.ecore.xmi.XMLResource,
	 *      java.util.Map)
	 */
	protected void init(XMLResource resource, Map opts) {
		super.init(resource, opts);

		if (escape != null) {
			// use MyEscape to ignore whitespaces
			//
			escape = new MyEscape();
		}

		saveSeparatelyClassSet = (Set) opts.get(SAVE_SEPARATELY_CLASS_SET);
		options = opts;

		ResourceSet resourceSet = resource.getResourceSet();
		if (resourceSet == null) {
			resourceSet = new MultiFileResourceSetImpl();
			resourceSet.getResources().add(resource);
		}
	}

	boolean canSaveSeparately(Object obj) {
		return MultiFileSaveUtil.hasOwnResource(obj, saveSeparatelyClassSet);
	}

	private String getUmaHREF(Resource resource, InternalEObject o) {
		if (o.eIsProxy()
				&& o.eProxyURI().scheme().equals(MultiFileURIConverter.SCHEME)) {
			return o.eProxyURI().toString();
		}
		String href = null;
		if (o instanceof MethodElement) {
			href = helper.getHREF(o);
			// TODO: need to check why helper.getHREF() returns null for some
			// MethodElements
		}
		if (href == null) {
			return MultiFileSaveUtil.getHREF(resource, o);
		}
		return href;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.eclipse.emf.ecore.xmi.impl.XMLSaveImpl#saveHref(org.eclipse.emf.ecore.EObject,
	 *      org.eclipse.emf.ecore.EStructuralFeature)
	 */
	protected void saveHref(EObject remote, EStructuralFeature f) {
		if (f instanceof EReference && ((EReference) f).isContainment()
				&& !toDOM) {
			// if this HREF is a contained element, save its ID attribute so the
			// proxy can be cached in the
			// GUIDToElementMap for temporary use before it can be resolved.
			// This helps improve loading time.
			//
			String href = helper.getHREF(remote);
			if (href != null) {
				if (escapeURI != null) {
					href = escapeURI.convert(href);
				}
				String name = helper.getQName(f);
				doc.startElement(name);
				EClass eClass = remote.eClass();
				EClass expectedType = (EClass) f.getEType();
				if (saveTypeInfo ? xmlTypeInfo.shouldSaveType(eClass,
						expectedType, f) : eClass != expectedType
						&& expectedType.isAbstract()) {
					saveTypeAttribute(eClass);
				}
				String id = helper.getID(remote);
				if (id != null) {
					doc.addAttribute(idAttributeName, id);
				}
				doc.addAttribute(XMLResource.HREF, href);
				if (eObjectToExtensionMap != null) {
					processAttributeExtensions(remote);
					if (processElementExtensions(remote)) {
						doc.endElement();
					} else {
						doc.endEmptyElement();
					}
				} else {
					doc.endEmptyElement();
				}
			}
			return;
		}

		super.saveHref(remote, f);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.eclipse.emf.ecore.xmi.impl.XMLSaveImpl#saveFeatures(org.eclipse.emf.ecore.EObject)
	 */
	protected boolean saveFeatures(EObject o) {
		try {
			MultiFileXMIResourceImpl resource = (MultiFileXMIResourceImpl) o
					.eResource();
			if (resource != null) {
				EObject container = o.eContainer();
				InternalEObject internalEObject = ((InternalEObject) o);
				if (o instanceof MethodElement && canSaveSeparately(o)
						&& container != null) {
					boolean containsNewFeature = (resource == container
							.eResource());
					MethodElement e = (MethodElement) o;
					URI uri = MultiFileSaveUtil
							.createFileURI((MethodElement) o);
					if (containsNewFeature) {
						if (!internalEObject.eIsProxy()) {
							resource = (MultiFileXMIResourceImpl) MultiFileSaveUtil
									.save(o, uri, options);
						}
					} else {
						// check if there is a ResourceDescriptor for this
						// element
						// create a new one if needed
						//
						ResourceManager resMgr = MultiFileSaveUtil
								.getResourceManager(container.eResource());
						if (resMgr != null
								&& resMgr.getResourceDescriptor(e.getGuid()) == null) {
							// make sure that no other ResourceManager has a
							// ResourceDescriptor for this element
							//
							MultiFileURIConverter uriConverter = (MultiFileURIConverter) resource
									.getResourceSet().getURIConverter();
							if (uriConverter
									.findResourceDescriptor(e.getGuid()) == null) {
								MultiFileSaveUtil.registerWithResourceManager(
										resMgr, e, resource.getFinalURI());
							}
						}
					}

					String href = getUmaHREF(resource, internalEObject);
					doc.addAttribute(XMLResource.HREF, href);
					endSaveFeatures(o, 0, null);
					return true;
				}
				if (o instanceof ResourceManager
						&& helper.getResource() != resource) {
					String href = getUmaHREF(resource, internalEObject);
					doc.addAttribute(XMLResource.HREF, href);
					endSaveFeatures(o, 0, null);
					return true;
				}
			}
			return super.saveFeatures(o);
		} catch (RuntimeException e) {
			CommonPlugin.INSTANCE.log(e);
			if (MultiFileSaveUtil.DEBUG) {
				e.printStackTrace();
				System.err.println("ERROR saving feature: " + o); //$NON-NLS-1$
				System.err.println("  Feature resource: " + o.eResource()); //$NON-NLS-1$
			}
			throw e;
		}
	}
	
	/* (non-Javadoc)
	 * @see org.eclipse.emf.ecore.xmi.impl.XMISaveImpl#addNamespaceDeclarations()
	 */
	public void addNamespaceDeclarations() {
		super.addNamespaceDeclarations();
		
		if (!toDOM) {
			// save tool version info
			//
			for (Iterator iter = VersionUtil.getAllToolIDs().iterator();iter.hasNext();) {
				String toolID = (String)iter.next();
				String toolVersion = VersionUtil.getVersions(toolID).getMinToolVersionForCurrentLibraryVersion().getToolVersion().toString();
				doc.addAttribute(toolID + ":version", toolVersion); //$NON-NLS-1$
			}
		}
	}

	/*
	 * protected String getDatatypeValue(Object value, EStructuralFeature f,
	 * boolean isAttribute) { if (value != null && value instanceof String) {
	 * String str = (String)value; if (str.indexOf("\r") > 0) { str =
	 * str.replaceAll("\r", ""); return super.getDatatypeValue(str, f,
	 * isAttribute); } }; return super.getDatatypeValue(value, f, isAttribute); }
	 */

	// TODO: needs to revisit this class since its base class has been updated a
	// lot in EMF 2.2
	public static class MyEscape extends Escape {
		protected final char[] GREATER = { '&', 'g', 't', ';' };

		protected final char[] BLANK = {};

		public MyEscape() {
			super();
		}

		/*
		 * Converts: & to &amp; < to &lt; " to &quot; \t to &#x9; \n to &#xA; \r
		 * to &#xD;
		 */
		public String convert(String input) {
			boolean changed = false;
			int inputLength = input.length();
			grow(inputLength);
			input.getChars(0, inputLength, value, 0);
			int pos = 0;
			char ch = 0;
			while (inputLength-- > 0) {
				ch = value[pos];
				switch (ch) {
				case '&':
					pos = replace(pos, AMP, inputLength);
					changed = true;
					break;
				case '<':
					pos = replace(pos, LESS, inputLength);
					changed = true;
					break;
				case '>':
					pos = replace(pos, GREATER, inputLength);
					changed = true;
					break;
				case '"':
					pos = replace(pos, QUOTE, inputLength);
					changed = true;
					break;
				default:
					/*
					 * if (!XMLChar.isValid(ch)) { throw new
					 * RuntimeException("An invalid XML character (Unicode: 0x" +
					 * Integer.toHexString(ch)+") was found in the element
					 * content:" +input); }
					 */
					pos++;
					break;
				}
			}
			return changed ? new String(value, 0, pos) : input;
		}

		/**
		 * Converts: & to &amp; < to &lt; > to &gt; " to &quot; \n to
		 * platform-dependent line breaking character
		 */
		public String convertText(String input) {
			boolean changed = false;
			int inputLength = input.length();
			grow(inputLength);
			input.getChars(0, inputLength, value, 0);
			int pos = 0;
			while (inputLength-- > 0) {
				switch (value[pos]) {
				case '&':
					pos = replace(pos, AMP, inputLength);
					changed = true;
					break;
				case '<':
					pos = replace(pos, LESS, inputLength);
					changed = true;
					break;
				case '>':
					pos = replace(pos, GREATER, inputLength);
					changed = true;
					break;
				case '"':
					pos = replace(pos, QUOTE, inputLength);
					changed = true;
					break;
				case '\r':
					pos = replace(pos, BLANK, inputLength);
					changed = true;
					break;
				case '\n':
					pos = replace(pos, LINE_FEED, inputLength);
					changed = true;
					break;
				default:
					pos++;
					break;
				}
			}
			return changed ? new String(value, 0, pos) : input;
		}

		/*
		 * Taken from XMLSaveImpl$Escape.convertText, and added conversion of >
		 * This is used for XLIFF's - change it's name so it doesn't impact EMF
		 * saving Convert: & to &amp; < to &lt; " to &quot; \n to line separator
		 */
		public String myConvertText(String input) {
			boolean changed = false;
			int inputLength = input.length();
			grow(inputLength);
			input.getChars(0, inputLength, value, 0);
			int pos = 0;
			while (inputLength-- > 0) {
				switch (value[pos]) {
				case '&':
					pos = replace(pos, AMP, inputLength);
					changed = true;
					break;
				case '<':
					pos = replace(pos, LESS, inputLength);
					changed = true;
					break;
				case '>':
					pos = replace(pos, GREATER, inputLength);
					changed = true;
					break;
				case '"':
					pos = replace(pos, QUOTE, inputLength);
					changed = true;
					break;
				case '\n': {
					pos = replace(pos, LINE_FEED, inputLength);
					changed = true;
					break;
				}
				default:
					pos++;
					break;
				}
			}

			return changed ? new String(value, 0, pos) : input;
		}
	}

	public static boolean checkModifyRequired(Map options) {
		Object opt = options.get(MultiFileXMISaveImpl.CHECK_MODIFY);
		return opt != null ? Boolean.valueOf(opt.toString()).booleanValue()
				: false;
	}
}
