package tk.eclipse.plugin.xmleditor.editors;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.xml.transform.Result;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;

import jp.aonir.fuzzyxml.FuzzyXMLDocType;
import jp.aonir.fuzzyxml.FuzzyXMLDocument;
import jp.aonir.fuzzyxml.FuzzyXMLElement;
import jp.aonir.fuzzyxml.FuzzyXMLNode;
import jp.aonir.fuzzyxml.FuzzyXMLParser;
import jp.aonir.fuzzyxml.internal.RenderContext;
import jp.aonir.fuzzyxml.internal.WOHTMLRenderDelegate;

import org.apache.xerces.parsers.SAXParser;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IMarker;
import org.eclipse.core.resources.IWorkspaceRunnable;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.jface.action.Action;
import org.eclipse.jface.action.IMenuManager;
import org.eclipse.jface.action.Separator;
import org.eclipse.jface.preference.IPreferenceStore;
import org.eclipse.jface.resource.StringConverter;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.ITextSelection;
import org.eclipse.jface.util.PropertyChangeEvent;
import org.eclipse.swt.SWT;
import org.eclipse.swt.widgets.FileDialog;
import org.eclipse.ui.IEditorInput;
import org.eclipse.ui.IFileEditorInput;
import org.objectstyle.wolips.wodclipse.core.Activator;
import org.objectstyle.wolips.wodclipse.core.preferences.PreferenceConstants;
import org.xml.sax.InputSource;

import tk.eclipse.plugin.htmleditor.HTMLPlugin;
import tk.eclipse.plugin.htmleditor.HTMLProjectParams;
import tk.eclipse.plugin.htmleditor.HTMLUtil;
import tk.eclipse.plugin.htmleditor.editors.HTMLConfiguration;
import tk.eclipse.plugin.htmleditor.editors.HTMLSourceEditor;
import tk.eclipse.plugin.htmleditor.editors.IHTMLOutlinePage;

/**
 * The XML editor.
 * 
 * @author Naoki Takezoe
 */
public class XMLEditor extends HTMLSourceEditor {
	
	private ArrayList<IDTDResolver> resolvers = new ArrayList<IDTDResolver>();
	
	public static final String GROUP_XML = "_xml";
	public static final String ACTION_GEN_DTD = "_generate_dtd";
	public static final String ACTION_GEN_XSD = "_generate_xsd";
	public static final String ACTION_ESCAPE_XML = "_escape_xml";
	public static final String ACTION_FORMAT_XML = "_format_xml";
	
	private String[] _classNameAttributes = null;
	private List<ElementSchemaMapping> _schemaMappings = null;
	
	/**
	 * The constructor.
	 */
	public XMLEditor() {
		this(new XMLConfiguration(HTMLPlugin.getDefault().getColorProvider()));
	}
	
	/**
	 * The constructor for customize this editor.
	 * <p>
	 * This editor is initialized with the given <code>XMLConfiguration</code>.
	 * 
	 * @param config the editor configuration for this editor
	 */
	public XMLEditor(XMLConfiguration config){
		super(config);
		
		setAction(ACTION_GEN_DTD,new GenerateDTDAction());
		setAction(ACTION_GEN_XSD,new GenerateXSDAction());
		setAction(ACTION_ESCAPE_XML, new EscapeXMLAction());
	}
	
	@Override
  protected void createActions() {
	    super.createActions();
	    // add content content format action
		setAction(ACTION_FORMAT_XML, new FormatXMLAction());
	}
	
	/** This method is called when configuration is changed. */
	@Override
  protected void handlePreferenceStoreChanged(PropertyChangeEvent event){
		super.handlePreferenceStoreChanged(event);
		_classNameAttributes = null;
		_schemaMappings = null;
	}
	
	public List<ElementSchemaMapping> getSchemaMappings(){
		if(_schemaMappings==null){
			_schemaMappings = ElementSchemaMapping.loadFromPreference();
		}
		return _schemaMappings;
	}
	
	public String[] getClassNameAttributes(){
		if(_classNameAttributes==null){
			// Load classname attrs from the preference store
			IPreferenceStore store = getPreferenceStore();
			if(store.getBoolean(HTMLPlugin.PREF_ENABLE_CLASSNAME)){
				_classNameAttributes = StringConverter.asArray(
						store.getString(HTMLPlugin.PREF_CLASSNAME_ATTRS));
			} else {
				_classNameAttributes = new String[0];
			}
		}
		return _classNameAttributes;
	}
	
	/**
	 * Returns the <code>XMLOutlinePage</code>.
	 * 
	 * @see XMLOutlinePage
	 */
	@Override
  protected IHTMLOutlinePage createOutlinePage(){
		return new XMLOutlinePage(this);
	}

	/**
	 * Adds <code>IDTDResolver</code>.
	 * 
	 * @param resolver IDTDResolver
	 */
	public void addDTDResolver(IDTDResolver resolver){
		resolvers.add(resolver);
	}
	
	/**
	 * Returns an array of <code>IDTDResolver</code> 
	 * that was added by <code>addEntityResolver()</code>.
	 * 
	 * @return an array of <code>IDTDResolver</code>
	 */
	public IDTDResolver[] getDTDResolvers(){
		return resolvers.toArray(new IDTDResolver[resolvers.size()]);
	}
	
	/**
	 * Validates the XML document.
	 * <p>
	 * If <code>getValidation()</code> returns <code>false</code>,
	 * this method do nothing.
	 */
	@Override
  protected void doValidate(){
		try {
			ResourcesPlugin.getWorkspace().run(new IWorkspaceRunnable() {
				public void run(IProgressMonitor monitor) throws CoreException {
					try {
						IFileEditorInput input = (IFileEditorInput)getEditorInput();
						String xml = getDocumentProvider().getDocument(input).get();
						IFile resource = input.getFile();
						//String charset = resource.getCharset();
						//charset = "Shift_JIS";
						resource.deleteMarkers(IMarker.PROBLEM,false,0);
						
						HTMLProjectParams params = new HTMLProjectParams(resource.getProject());
						if(!params.getValidateXML()){
							return;
						}
						
						if(params.getUseDTD()==false){
							// remove DOCTYPE decl
							Matcher matcher = patternDoctypePublic.matcher(xml);
							if(matcher.find()){
								xml = removeMatched(xml,matcher.start(),matcher.end());
							}
							matcher = patternDoctypeSystem.matcher(xml);
							if(matcher.find()){
								xml = removeMatched(xml,matcher.start(),matcher.end());
							}
						}
						
						SAXParser parser = new SAXParser();
						
						String   dtd = getDTD(xml, false);
						String[] xsd = getXSD(xml, false);
						
						// Validation configuration
						if((dtd==null && xsd==null) || !params.getUseDTD()){
							parser.setFeature("http://xml.org/sax/features/validation", false);
						} else {
							parser.setFeature("http://xml.org/sax/features/validation", true);
							parser.setFeature("http://apache.org/xml/features/continue-after-fatal-error", true);
						}
						if(xsd!=null && params.getUseDTD()){
//							parser.setProperty("http://java.sun.com/xml/jaxp/properties/schemaLanguage", "http://www.w3.org/2001/XMLSchema");
//							parser.setProperty("http://java.sun.com/xml/jaxp/properties/schemaSource", xsd);
							parser.setFeature("http://apache.org/xml/features/validation/schema", true);
							parser.setFeature("http://xml.org/sax/features/namespaces", true);
						}
						
						parser.setFeature("http://xml.org/sax/features/use-entity-resolver2", true);
						parser.setEntityResolver(new DTDResolver(getDTDResolvers(),
								input.getFile().getLocation().makeAbsolute().toFile().getParentFile()));
						parser.setErrorHandler(new XMLValidationHandler(resource));
						
						parser.parse(new InputSource(new StringReader(xml))); //new ByteArrayInputStream(xml.getBytes(charset))));
						
					} catch(Exception ex){
						// ignore
						//HTMLPlugin.logException(ex);
					}
				}
			},null);
		} catch(Exception ex){
			HTMLPlugin.logException(ex);
		}
	}
	
	/** replace to whitespaces */
	private String removeMatched(String source,int start,int end){
		StringBuffer sb = new StringBuffer();
		sb.append(source.substring(0,start));
		for(int i=start;i<end + 1;i++){
			char c = source.charAt(i);
			if(c=='\r' || c=='\n'){
				sb.append(c);
			} else {
				sb.append(" ");
			}
		}
		sb.append(source.substring(end+1,source.length()));
		return sb.toString();
	}
	
	/**
	 * Returns URI of DTD (SystemID) which is used in the document.
	 * If any DTD isn't used, this method returns <code>null</code>.
	 * 
	 * @param xml XML
	 * @return URL of DTD
	 */
	public String getDTD(String xml, boolean useElementMapping){
		// PUBLIC Identifier
		Matcher matcher = patternDoctypePublic.matcher(xml);
		if(matcher.find()){
			return matcher.group(2);
		}
		// SYSTEM Identifier
		matcher = patternDoctypeSystem.matcher(xml);
		if(matcher.find()){
			return matcher.group(1);
		}
		
		// Root element mappings
		if(useElementMapping){
			String firstTag = getFirstTag(xml);
			if(firstTag!=null){
				List<ElementSchemaMapping> schemaMappings = getSchemaMappings();
				for(int i=0;i<schemaMappings.size();i++){
					ElementSchemaMapping mapping = schemaMappings.get(i);
					if(mapping.getRootElement().equals(firstTag) && mapping.getFilePath().endsWith(".dtd")){
						return "file:" + mapping.getFilePath();
					}
				}
			}
		}
		
		return null;
	}
	
	/**
	 * Returns URI (schema location) of XML schema which is used in the document.
	 * If any XML schema isn't used, this method returns <code>null</code>.
	 * 
	 * @param xml XML
	 * @return URL of XML schema
	 */
	public String[] getXSD(String xml, boolean useElementMapping){
		// PUBLIC Identifier
		Matcher matcher = patternNsXSD.matcher(xml);
		if(matcher.find()){
			String matched = matcher.group(1).trim();
			matched.replaceAll("\r\n","\n");
			matched.replaceAll("\r","\n");
			String[] xsd = matched.split("\n| |\t");
			for(int i=0;i<xsd.length;i++){
				xsd[i] = xsd[i].trim();
			}
			return xsd;
		}
		matcher = patternNoNsXSD.matcher(xml);
		if(matcher.find()){
			return new String[]{matcher.group(3).trim()};
		}
		
		// Root element mappings
		if(useElementMapping){
			String firstTag = getFirstTag(xml);
			if(firstTag!=null){
				List schemaMappings = getSchemaMappings();
				for(int i=0;i<schemaMappings.size();i++){
					ElementSchemaMapping mapping = (ElementSchemaMapping)schemaMappings.get(i);
					if(mapping.getRootElement().equals(firstTag) && mapping.getFilePath().endsWith(".xsd")){
						return new String[]{ "file:" + mapping.getFilePath() };
					}
				}
			}
		}
		
		return null;
	}
	
	/**
	 * Extracts the first element name in the given xml source.
	 */
	private static String getFirstTag(String xml){
		FuzzyXMLDocument doc = new FuzzyXMLParser(false).parse(xml);
		FuzzyXMLNode[] nodes = doc.getDocumentElement().getChildren();
		for(int i=0;i<nodes.length;i++){
			if(nodes[i] instanceof FuzzyXMLElement){
				return ((FuzzyXMLElement)nodes[i]).getName();
			}
		}
		return null;
	}
	
	/** Reular expressions to get DOCTYPE declaration */
	private Pattern patternDoctypePublic
		= Pattern.compile("<!DOCTYPE[\\s\r\n]+?[^<]+?[\\s\r\n]+?PUBLIC[\\s\r\n]*?\"(.+?)\"[\\s\r\n]*?\"(.+?)\".*?>",Pattern.DOTALL);
	private Pattern patternDoctypeSystem
		= Pattern.compile("<!DOCTYPE[\\s\r\n]+?[^<]+?[\\s\r\n]+?SYSTEM[\\s\r\n]*?\"(.+?)\".*?>",Pattern.DOTALL);
	
	/** Reular expressions to get schema location of XMLschema */
	private Pattern patternNsXSD 
		= Pattern.compile("schemaLocation[\\s\r\n]*?=[\\s\r\n]*?\"(.+?)\"",Pattern.DOTALL);
	private Pattern patternNoNsXSD 
		= Pattern.compile("noNamespaceSchemaLocation[\\s\r\n]*?=[\\s\r\n]*?\"(.+?)\"",Pattern.DOTALL);
	
	/**
	 * Update informations about code-completion.
	 */
	@Override
  protected void updateAssist(){
		try {
			XMLConfiguration config = (XMLConfiguration)getSourceViewerConfiguration();
			config.getClassNameHyperlinkProvider().setEditor(this);
			
			if(!isFileEditorInput()){
				return;
			}
			super.updateAssist();
			
			IFileEditorInput input = (IFileEditorInput)getEditorInput();
			HTMLProjectParams params = new HTMLProjectParams(input.getFile().getProject());
			if(params.getUseDTD()==false){
				return;
			}
			
			String xml = getDocumentProvider().getDocument(input).get();
			
			// Update DTD based completion information.
			String dtd = getDTD(xml, true);
			if(dtd!=null){
				DTDResolver resolver = new DTDResolver(getDTDResolvers(), 
						input.getFile().getLocation().makeAbsolute().toFile().getParentFile());
				InputStream in = resolver.getInputStream(dtd);
				if(in!=null){
					Reader reader = new InputStreamReader(in);
					// update AssistProcessor
					XMLAssistProcessor assistProcessor = 
						(XMLAssistProcessor)((HTMLConfiguration)getSourceViewerConfiguration()).getAssistProcessor();
					assistProcessor.updateDTDInfo(reader);
					reader.close();
				}
			}
			
			// Update XML Schema based completion information.
			String[] xsd = getXSD(xml, true);
			if(xsd!=null){
				DTDResolver resolver = new DTDResolver(getDTDResolvers(),
						input.getFile().getLocation().makeAbsolute().toFile().getParentFile());
				for(int i=0;i<xsd.length;i++){
					InputStream in = resolver.getInputStream(xsd[i]);
					if(in!=null){
						Reader reader = new InputStreamReader(in);
						// update AssistProcessor
						XMLAssistProcessor assistProcessor = 
							(XMLAssistProcessor)((HTMLConfiguration)getSourceViewerConfiguration()).getAssistProcessor();
						assistProcessor.updateXSDInfo(xsd[i],reader);
						reader.close();
					}
				}
			}
		} catch(Exception ex){
			HTMLPlugin.logException(ex);
		}
	}
	
	@Override
  protected void addContextMenuActions(IMenuManager menu){
		menu.add(new Separator(GROUP_HTML));
		//addAction(menu,GROUP_HTML,ACTION_OPEN_PALETTE);
		addAction(menu,GROUP_HTML,ACTION_ESCAPE_XML);
		addAction(menu,GROUP_HTML,ACTION_COMMENT);
		addAction(menu,GROUP_HTML,ACTION_FORMAT_XML);
		menu.add(new Separator(GROUP_XML));
		addAction(menu,GROUP_XML,ACTION_GEN_DTD);
		addAction(menu,GROUP_XML,ACTION_GEN_XSD);
	}
	
	@Override
  protected void updateSelectionDependentActions() {
		super.updateSelectionDependentActions();
		ITextSelection sel = (ITextSelection)getSelectionProvider().getSelection();
		if(sel.getText().equals("")){
			getAction(ACTION_ESCAPE_XML).setEnabled(false);
		} else {
			getAction(ACTION_ESCAPE_XML).setEnabled(true);
		}
	}
	
	////////////////////////////////////////////////////////////////////////////
	// actions
	////////////////////////////////////////////////////////////////////////////	
	/**
	 * The action to escape XML special chars in the selection. 
	 */
	private class EscapeXMLAction extends Action {
		
		public EscapeXMLAction(){
			super(HTMLPlugin.getResourceString("HTMLEditor.EscapeAction"));
			setEnabled(false);
			setAccelerator(SWT.CTRL | '\\');
		}
		
		@Override
    public void run() {
			ITextSelection sel = (ITextSelection)getSelectionProvider().getSelection();
			IDocument doc = getDocumentProvider().getDocument(getEditorInput());
			try {
				doc.replace(sel.getOffset(),sel.getLength(),HTMLUtil.escapeXML(sel.getText()));
			} catch (BadLocationException e) {
				HTMLPlugin.logException(e);
			}
		}
	}
	
	/**
	 * The action to generate DTD from XML.
	 */
	private class GenerateDTDAction extends Action {
		public GenerateDTDAction(){
			super(HTMLPlugin.getResourceString("XMLEditor.GenerateDTD"),
					HTMLPlugin.getDefault().getImageRegistry().getDescriptor(HTMLPlugin.ICON_DTD));
		}
		@Override
    public void run() {
			FileDialog dialog = new FileDialog(getViewer().getTextWidget().getShell(),SWT.SAVE);
			dialog.setFilterExtensions(new String[]{"*.dtd"});
			String file = dialog.open();
			if(file!=null){
				try {
					SchemaGenerator.generateDTDFromXML(getFile(), new File(file));
				} catch(Exception ex){
					HTMLPlugin.openAlertDialog(ex.toString());
				}
			}
		}
	}
	
	/**
	 * The action to generate XML schema from XML.
	 */
	private class GenerateXSDAction extends Action {
		public GenerateXSDAction(){
			super(HTMLPlugin.getResourceString("XMLEditor.GenerateXSD"),
					HTMLPlugin.getDefault().getImageRegistry().getDescriptor(HTMLPlugin.ICON_XSD));
		}
		@Override
    public void run() {
			FileDialog dialog = new FileDialog(getViewer().getTextWidget().getShell(),SWT.SAVE);
			dialog.setFilterExtensions(new String[]{"*.xsd"});
			String file = dialog.open();
			if(file!=null){
				try {
					SchemaGenerator.generateXSDFromXML(getFile(), new File(file));
				} catch(Exception ex){
					HTMLPlugin.openAlertDialog(ex.toString());
				}
			}
		}
	}
	
	/**
	 * The action to format (correct indentation) XML.
	 */
	private class FormatXMLAction extends Action {
		public FormatXMLAction(){
			super(HTMLPlugin.getResourceString("XMLEditor.FormatXML"));
			setActionDefinitionId("tk.eclipse.plugin.xmleditor.format");
		}
		@Override
    public void run(){
			try {
			  IDocument xmlDocument = getDocumentProvider().getDocument(getEditorInput());
			  String xmlString = xmlDocument.get();
	      FuzzyXMLDocument htmlModel = new FuzzyXMLParser(false, false).parse(xmlString);
	      FuzzyXMLElement documentElement = htmlModel.getDocumentElement();

	      IPreferenceStore prefs = Activator.getDefault().getPreferenceStore();
	      RenderContext renderContext = new RenderContext(true);
	      renderContext.setShowNewlines(true);
	      renderContext.setIndentSize(prefs.getInt(PreferenceConstants.INDENT_SIZE));
	      renderContext.setIndentTabs(prefs.getBoolean(PreferenceConstants.INDENT_TABS));
	      renderContext.setTrim(true);
	      renderContext.setLowercaseAttributes(prefs.getBoolean(PreferenceConstants.LOWERCASE_ATTRIBUTES));
	      renderContext.setLowercaseTags(prefs.getBoolean(PreferenceConstants.LOWERCASE_TAGS));
	      renderContext.setSpacesAroundEquals(prefs.getBoolean(PreferenceConstants.SPACES_AROUND_EQUALS));
	      renderContext.setSpaceInEmptyTags(true);
	      renderContext.setAddMissingQuotes(true);
	      renderContext.setDelegate(new WOHTMLRenderDelegate(prefs.getBoolean(PreferenceConstants.STICKY_WOTAGS)));

	      StringBuffer xmlBuffer = new StringBuffer();
	      FuzzyXMLDocType docType = htmlModel.getDocumentType();
	      if (docType != null) {
	        docType.toXMLString(renderContext, xmlBuffer);
	      }
	      for (FuzzyXMLNode node : documentElement.getChildren()) {
	        node.toXMLString(renderContext, xmlBuffer);
	        //htmlBuffer.append("\n");
	      }
	      xmlDocument.set(xmlBuffer.toString().trim());

//				// Format XML using XSL
//				IEditorInput input = getEditorInput();
//				String charset = System.getProperty("file.encoding");
//				if(input instanceof IFileEditorInput){
//					charset = ((IFileEditorInput)input).getFile().getCharset();
//				}
//				
//				TransformerFactory transFactory = TransformerFactory.newInstance();
//				
//				String xsl = new String(HTMLUtil.readStream(
//						XMLEditor.class.getResourceAsStream("format.xsl")));
//				xsl = xsl.replaceAll("\\$\\{charset\\}", charset);
//				
//				Source xslSource = new StreamSource(new StringReader(xsl));
//				Transformer transformer = transFactory.newTransformer(xslSource);
//				
//				IDocument doc = getDocumentProvider().getDocument(getEditorInput());
//				ByteArrayOutputStream out = new ByteArrayOutputStream();
//				
//				
//				Source source = new StreamSource(new StringReader(doc.get()));
//				Result result = new StreamResult(out);
//				
//				transformer.transform(source, result);
				
				//doc.set(new String(out.toByteArray(), charset));
				
			} catch(Exception ex){
				HTMLPlugin.openAlertDialog(ex.toString());
			}
		}
	}
}
