/*******************************************************************************
 * Copyright (c) 2004, 2009 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 API and implementation
 *     
 * 	    Masaki Saitoh (MSAITOH@jp.ibm.com)
 *		See Bug 153000  Style Adapters should be lazier
 *		https://bugs.eclipse.org/bugs/show_bug.cgi?id=153000
 *
 ********************************************************************************/
package org.eclipse.wst.html.core.internal.htmlcss;



import org.eclipse.wst.css.core.internal.provisional.adapters.IModelProvideAdapter;
import org.eclipse.wst.css.core.internal.provisional.adapters.IStyleSheetListAdapter;
import org.eclipse.wst.css.core.internal.provisional.document.ICSSModel;
import org.eclipse.wst.html.core.internal.provisional.HTML40Namespace;
import org.eclipse.wst.sse.core.internal.provisional.INodeNotifier;
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.text.IStructuredDocument;
import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocumentRegion;
import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocumentRegionList;
import org.eclipse.wst.xml.core.internal.provisional.document.IDOMModel;
import org.eclipse.wst.xml.core.internal.provisional.document.IDOMNode;
import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;

/**
 */
public class StyleElementAdapter extends AbstractStyleSheetAdapter implements IStructuredDocumentListener {

	private boolean replaceModel = true;
	private boolean ignoreNotification = false;

	/**
	 */
	protected StyleElementAdapter() {
		super();
	}

	/**
	 * Preparation of applying changes from CSS sub-model to HTML model
	 */
	private void changeStructuredDocumentRegion(IStructuredDocumentRegion flatNode) {
		if (flatNode == null)
			return;
		Element element = getElement();
		if (element == null)
			return;
		ICSSModel model = getExistingModel();
		if (model == null)
			return;
		IStructuredDocument structuredDocument = model.getStructuredDocument();
		if (structuredDocument == null)
			return;

		// get old content length
		Node child = element.getFirstChild();
		if (child == null || child.getNodeType() != Node.TEXT_NODE)
			return;
		IDOMNode content = (IDOMNode) child;
		int oldLength = content.getEndOffset() - content.getStartOffset();

		// get new content length
		int newLength = 0;
		IStructuredDocumentRegionList flatNodes = structuredDocument.getRegionList();
		if (flatNodes != null) {
			int count = flatNodes.getLength();
			if (count > 0) {
				IStructuredDocumentRegion last = flatNodes.item(count - 1);
				if (last != null)
					newLength = last.getEnd();
			}
		}

		int offset = flatNode.getStart();
		int end = flatNode.getEnd();
		int diff = oldLength - newLength;
		int length = end - offset + diff;
		String data = flatNode.getText();

		replaceData(offset, length, data);
	}

	/**
	 * Apply changes from HTML model to CSS sub-model
	 */
	private void contentChanged() {
		Element element = getElement();
		if (element == null)
			return;
		ICSSModel model = getExistingModel();
		if (model == null)
			return;
		IStructuredDocument structuredDocument = model.getStructuredDocument();
		if (structuredDocument == null)
			return;

		String data = null;
		Node child = element.getFirstChild();
		if (child != null && child.getNodeType() == Node.TEXT_NODE && child.getNextSibling() == null) {
			data = child.getNodeValue();
		}
		if (data == null)
			data = "";//$NON-NLS-1$

		// minimize replace range
		int start = 0, end = 0;
		String oldData = structuredDocument.get();
		if (oldData == null)
			oldData = "";//$NON-NLS-1$

		// search differenct character position from first
		for (; start < oldData.length() && start < data.length(); start++)
			if (oldData.charAt(start) != data.charAt(start))
				break;

		if (start == oldData.length() && start == data.length())
			return; // no change
		else if (start == oldData.length()) {
			structuredDocument.replaceText(getRequesterH2C(), start, 0, data.substring(start)); // append text to last
		}
		else if (start == data.length()) {
			structuredDocument.replaceText(getRequesterH2C(), start, oldData.length() - start, ""); // remove text of last //$NON-NLS-1$
		}
		else {
			// search differenct character position from last
			for (; start < oldData.length() - end && start < data.length() - end; end++) {
				if (oldData.charAt(oldData.length() - end - 1) != data.charAt(data.length() - end - 1))
					break;
			}
			structuredDocument.replaceText(getRequesterH2C(), start, oldData.length() - end - start, data.substring(start, data.length() - end));
		}

	}

	/**
	 */
	public ICSSModel getModel() {
		ICSSModel model = getExistingModel();
		if (this.replaceModel) {
			ICSSModel oldModel = model;
			model = createModel(false);

			setModel(model, false); // need to set before contentChanged()
			contentChanged();

			// from super.createModel()
			// get ModelProvideAdapter
			IModelProvideAdapter modelProvideAdapter = (IModelProvideAdapter) ((INodeNotifier) getElement()).getAdapterFor(IModelProvideAdapter.class);
			// notify adapter
			if (modelProvideAdapter != null)
				modelProvideAdapter.modelProvided(model);

			// from createModel()
			IStructuredDocument structuredDocument = model.getStructuredDocument();
			if (structuredDocument == null)
				return null;
			structuredDocument.addDocumentChangedListener(this);

			// from setModel()
			if (oldModel != null)
				oldModel.removeStyleListener(this);
			if (model != null)
				model.addStyleListener(this);

			if (oldModel != null) {
				// get ModelProvideAdapter
				IModelProvideAdapter adapter = (IModelProvideAdapter) ((INodeNotifier) getElement()).getAdapterFor(IModelProvideAdapter.class);
				if (adapter != null) {
					adapter.modelRemoved(oldModel);
				}
			}

			this.replaceModel = false;
		}
		return model;
	}

	/**
	 */
	protected boolean isValidAttribute() {
		Element element = getElement();
		if (element == null) {
			return false;
		}
		String type = element.getAttribute(HTML40Namespace.ATTR_NAME_TYPE);
		if (type != null && type.length() > 0 && !type.equalsIgnoreCase("text/css")) { //$NON-NLS-1$
			return false;
		}
		return true;
	}

	/**
	 */
	protected ICSSModel createModel() {
		return createModel(true);
	}

	/**
	 */
	protected ICSSModel createModel(boolean addListener) {
		if (!isValidAttribute()) {
			return null;
		}

		ICSSModel model = super.createModel(addListener);

		if (!addListener)
			return model;

		IStructuredDocument structuredDocument = model.getStructuredDocument();
		if (structuredDocument == null)
			return null;
		structuredDocument.addDocumentChangedListener(this);

		return model;
	}

	/**
	 */
	//  public ICSSModel getModel() {
	//  	ICSSModel model = getExistingModel();
	//  	if (model == null) {
	//  		model = createModel();
	//  		if (model == null) return null;
	//  		IStructuredDocument structuredDocument = model.getStructuredDocument();
	//  		if (structuredDocument == null) return null;
	//  		structuredDocument.addModelChangedListener(this);
	//  		setModel(model); // need to set before contentChanged()
	//  		contentChanged();
	//  	}
	//  	return model;
	//  }
	/**
	 */
	private Object getRequesterH2C() {
		return (getElement() != null && ((IDOMNode) getElement()).getModel() != null) ? (Object) ((IDOMNode) getElement()).getModel() : this;
	}

	/**
	 */
	private Object getRequesterC2H() {
		return (getModel() != null) ? (Object) getModel() : this;
	}

	/**
	 * Implementing IStructuredDocumentListener's method
	 * Event from CSS Flat Model
	 */
	public void newModel(NewDocumentEvent event) {
		if (event == null)
			return;
		if (event.getOriginalRequester() == getRequesterH2C())
			return;
		IStructuredDocument structuredDocument = event.getStructuredDocument();
		if (structuredDocument == null)
			return;
		IStructuredDocumentRegionList flatNodes = structuredDocument.getRegionList();
		if (flatNodes == null)
			return;

		replaceStructuredDocumentRegions(flatNodes, null);
	}

	/**
	 * Implementing IStructuredDocumentListener's method
	 * Event from CSS Flat Model
	 */
	public void noChange(NoChangeEvent structuredDocumentEvent) {
	}

	/**
	 * Implementing IStructuredDocumentListener's method
	 * Event from CSS Flat Model
	 */
	public void nodesReplaced(StructuredDocumentRegionsReplacedEvent event) {
		if (event == null)
			return;
		if (event.getOriginalRequester() == getRequesterH2C())
			return;
		IStructuredDocumentRegionList oldStructuredDocumentRegions = event.getOldStructuredDocumentRegions();
		IStructuredDocumentRegionList newStructuredDocumentRegions = event.getNewStructuredDocumentRegions();
		if (oldStructuredDocumentRegions == null && newStructuredDocumentRegions == null)
			return;

		replaceStructuredDocumentRegions(newStructuredDocumentRegions, oldStructuredDocumentRegions);
	}

	/**
	 * Overriding INodeAdapter's method
	 * Event from <STYLE> element
	 */
	public void notifyChanged(INodeNotifier notifier, int eventType, Object changedFeature, Object oldValue, Object newValue, int pos) {
		if (this.ignoreNotification)
			return;

		if (eventType == INodeNotifier.ADD || eventType == INodeNotifier.REMOVE || eventType == INodeNotifier.CONTENT_CHANGED) {
			contentChanged();
		}
		else if (eventType == INodeNotifier.CHANGE) {
			Attr attr = (Attr) changedFeature;
			if (attr == null)
				return;
			String name = attr.getName();
			if (name.equalsIgnoreCase("type")) { //$NON-NLS-1$
				this.replaceModel = true;

				Element element = getElement();
				if (element == null) {
					return;
				}
				Document document = element.getOwnerDocument();
				if (document == null) {
					return;
				}
				HTMLDocumentAdapter adapter = (HTMLDocumentAdapter) ((INodeNotifier) document).getAdapterFor(IStyleSheetListAdapter.class);
				if (adapter != null) {
					adapter.childReplaced();
				}
			}
		}
	}

	/**
	 * Implementing IStructuredDocumentListener's method
	 * Event from CSS Flat Model
	 */
	public void regionChanged(RegionChangedEvent event) {
		if (event == null)
			return;
		if (event.getOriginalRequester() == getRequesterH2C())
			return;
		IStructuredDocumentRegion flatNode = event.getStructuredDocumentRegion();
		if (flatNode == null)
			return;

		changeStructuredDocumentRegion(flatNode);
	}

	/**
	 * Implementing IStructuredDocumentListener's method
	 * Event from CSS Flat Model
	 */
	public void regionsReplaced(RegionsReplacedEvent event) {
		if (event == null)
			return;
		if (event.getOriginalRequester() == getRequesterH2C())
			return;
		IStructuredDocumentRegion flatNode = event.getStructuredDocumentRegion();
		if (flatNode == null)
			return;

		changeStructuredDocumentRegion(flatNode);
	}

	/**
	 * Apply changes from CSS sub-model to HTML model
	 */
	private void replaceData(int offset, int length, String data) {
		IDOMNode element = (IDOMNode) getElement();
		if (element == null)
			return;
		IDOMModel ownerModel = element.getModel();
		if (ownerModel == null)
			return;
		IStructuredDocument structuredDocument = ownerModel.getStructuredDocument();
		if (structuredDocument == null)
			return;
		IStructuredDocumentRegion flatNode = element.getStartStructuredDocumentRegion();
		if (flatNode == null)
			return;

		int contentOffset = flatNode.getEndOffset();
		if (data == null)
			data = "";//$NON-NLS-1$

		this.ignoreNotification = true;
		structuredDocument.replaceText(getRequesterC2H(), contentOffset + offset, length, data);
		this.ignoreNotification = false;
	}

	/**
	 * Preparation of applying changes from CSS sub-model to HTML model
	 */
	private void replaceStructuredDocumentRegions(IStructuredDocumentRegionList newStructuredDocumentRegions, IStructuredDocumentRegionList oldStructuredDocumentRegions) {
		int offset = 0;
		int length = 0;
		if (oldStructuredDocumentRegions != null) {
			int count = oldStructuredDocumentRegions.getLength();
			if (count > 0) {
				IStructuredDocumentRegion first = oldStructuredDocumentRegions.item(0);
				if (first != null)
					offset = first.getStart();
				IStructuredDocumentRegion last = oldStructuredDocumentRegions.item(count - 1);
				if (last != null)
					length = last.getEnd() - offset;
			}
		}
		String data = null;
		if (newStructuredDocumentRegions != null) {
			int count = newStructuredDocumentRegions.getLength();
			if (count > 0) {
				StringBuffer buffer = new StringBuffer();
				for (int i = 0; i < count; i++) {
					IStructuredDocumentRegion flatNode = newStructuredDocumentRegions.item(i);
					if (flatNode == null)
						continue;
					buffer.append(flatNode.getText());
					if (i == 0)
						offset = flatNode.getStart();
				}
				data = buffer.toString();
			}
		}

		replaceData(offset, length, data);
	}

	/**
	 */
	protected void setModel(ICSSModel model) {
		setModel(model, true);
	}

	/**
	 */
	protected void setModel(ICSSModel model, boolean setupListener) {
		ICSSModel oldModel = getExistingModel();
		if (model == oldModel)
			return;
		super.setModel(model);
		if (!setupListener)
			return;
		if (oldModel != null)
			oldModel.removeStyleListener(this);
		if (model != null)
			model.addStyleListener(this);
	}
}