package tk.eclipse.plugin.jseditor.editors;

import java.util.ArrayList;
import java.util.Stack;

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.IAction;
import org.eclipse.jface.action.IMenuManager;
import org.eclipse.jface.action.Separator;
import org.eclipse.jface.preference.IPreferenceStore;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.ITextSelection;
import org.eclipse.jface.text.ITextViewerExtension2;
import org.eclipse.jface.text.hyperlink.IHyperlinkDetector;
import org.eclipse.jface.text.source.ISourceViewer;
import org.eclipse.jface.text.source.IVerticalRuler;
import org.eclipse.jface.text.source.MatchingCharacterPainter;
import org.eclipse.jface.text.source.projection.ProjectionAnnotationModel;
import org.eclipse.jface.text.source.projection.ProjectionSupport;
import org.eclipse.jface.text.source.projection.ProjectionViewer;
import org.eclipse.jface.util.PropertyChangeEvent;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.StyledText;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Display;
import org.eclipse.ui.IEditorInput;
import org.eclipse.ui.IFileEditorInput;
import org.eclipse.ui.IStorageEditorInput;
import org.eclipse.ui.editors.text.TextEditor;
import org.eclipse.ui.texteditor.AbstractDecoratedTextEditorPreferenceConstants;
import org.eclipse.ui.texteditor.ChainedPreferenceStore;
import org.eclipse.ui.texteditor.ContentAssistAction;
import org.eclipse.ui.texteditor.ITextEditorActionDefinitionIds;
import org.eclipse.ui.views.contentoutline.IContentOutlinePage;

import tk.eclipse.plugin.htmleditor.ColorProvider;
import tk.eclipse.plugin.htmleditor.HTMLPlugin;
import tk.eclipse.plugin.htmleditor.HTMLProjectParams;
import tk.eclipse.plugin.htmleditor.editors.FoldingInfo;
import tk.eclipse.plugin.htmleditor.editors.SoftTabVerifyListener;

/**
 * The JavaScript editor.
 * 
 * @author Naoki Takezoe
 * @see tk.eclipse.plugin.jseditor.editors.JavaScriptOutlinePage
 * @see tk.eclipse.plugin.jseditor.editors.JavaScriptConfiguration
 * @see tk.eclipse.plugin.jseditor.editors.JavaScriptAssistProcessor
 * @see tk.eclipse.plugin.jseditor.editors.JavaScriptCharacterPairMatcher
 * @see tk.eclipse.plugin.jseditor.editors.JavaScriptHyperlinkDetector
 */
public class JavaScriptEditor extends TextEditor {

	private ColorProvider colorProvider;
	private SoftTabVerifyListener softTabListener;
	private JavaScriptOutlinePage outline;
	private JavaScriptCharacterPairMatcher pairMatcher;
	private ProjectionSupport fProjectionSupport;
	private JavaScriptHyperlinkDetector hyperlinkDetector;
	
	public static final String GROUP_JAVASCRIPT = "_javascript";
	public static final String ACTION_COMMENT = "_comment";
	
	/**
	 * The constructor.
	 */
	public JavaScriptEditor(){
		super();
		colorProvider = HTMLPlugin.getDefault().getColorProvider();
		setSourceViewerConfiguration(new JavaScriptConfiguration(colorProvider));
		setPreferenceStore(new ChainedPreferenceStore(
				new IPreferenceStore[]{
						getPreferenceStore(),
						HTMLPlugin.getDefault().getPreferenceStore()
				}));
		
		outline = new JavaScriptOutlinePage(this);
		
		IPreferenceStore store = HTMLPlugin.getDefault().getPreferenceStore();
		softTabListener = new SoftTabVerifyListener();
		softTabListener.setUseSoftTab(store.getBoolean(HTMLPlugin.PREF_USE_SOFTTAB));
		softTabListener.setSoftTabWidth(store.getInt(HTMLPlugin.PREF_SOFTTAB_WIDTH));
		
		setAction(ACTION_COMMENT,new CommentAction());
	}
	
	@Override
  protected void updateSelectionDependentActions() {
		super.updateSelectionDependentActions();
		ITextSelection sel = (ITextSelection)getSelectionProvider().getSelection();
		if(sel.getText().equals("")){
			getAction(ACTION_COMMENT).setEnabled(false);
		} else {
			getAction(ACTION_COMMENT).setEnabled(true);
		}
	}
	
	@Override
  protected ISourceViewer createSourceViewer(Composite parent,IVerticalRuler ruler, int styles) {
		ISourceViewer viewer= new ProjectionViewer(parent, ruler, getOverviewRuler(), true, styles); 
		getSourceViewerDecorationSupport(viewer);
		viewer.getTextWidget().addVerifyListener(softTabListener);
		return viewer; 
	}
	
	@Override
  protected final void editorContextMenuAboutToShow(IMenuManager menu) {
		super.editorContextMenuAboutToShow(menu);
		menu.add(new Separator(GROUP_JAVASCRIPT));
		addAction(menu, GROUP_JAVASCRIPT, ACTION_COMMENT);
	}
	
	@Override
  protected void doSetInput(IEditorInput input) throws CoreException {
		if(input instanceof IFileEditorInput){
			setDocumentProvider(new JavaScriptTextDocumentProvider());
		} else if(input instanceof IStorageEditorInput){
			setDocumentProvider(new JavaScriptFileDocumentProvider());
		} else {
			setDocumentProvider(new JavaScriptTextDocumentProvider());
		}
		super.doSetInput(input);
	}
	
	@Override
  public void doSave(IProgressMonitor progressMonitor) {
		super.doSave(progressMonitor);
		update();
	}
	
	@Override
  public void createPartControl(Composite parent) {
		super.createPartControl(parent);
		
		ProjectionViewer viewer = (ProjectionViewer)getSourceViewer();
		fProjectionSupport = new ProjectionSupport(viewer, getAnnotationAccess(), getSharedColors()); 
		fProjectionSupport.install();
		viewer.doOperation(ProjectionViewer.TOGGLE);
		updateFolding();
		
		StyledText widget = viewer.getTextWidget();
		widget.setTabs(
				getPreferenceStore().getInt(
						AbstractDecoratedTextEditorPreferenceConstants.EDITOR_TAB_WIDTH));
		widget.addVerifyListener(softTabListener);
		
		ITextViewerExtension2 extension= (ITextViewerExtension2) getSourceViewer();
		pairMatcher = new JavaScriptCharacterPairMatcher();
		pairMatcher.setEnable(getPreferenceStore().getBoolean(HTMLPlugin.PREF_PAIR_CHAR));
		MatchingCharacterPainter painter = new MatchingCharacterPainter(getSourceViewer(), pairMatcher);
		painter.setColor(Display.getDefault().getSystemColor(SWT.COLOR_GRAY));
		extension.addPainter(painter);
		
		hyperlinkDetector = new JavaScriptHyperlinkDetector();
		viewer.setHyperlinkDetectors(new IHyperlinkDetector[]{hyperlinkDetector}, SWT.CTRL);
		
		update();
	}
	
	/**
	 * Updates internal status.
	 * 
	 * <ol>
	 *   <li>updates the outline view</li>
	 *   <li>updates the foldings</li>
	 *   <li>validates contents if the editor input is <code>IFileEditorInput</code></li>
	 *   <li>updates assist information if the editor input is <code>IFileEditorInput</code></li>
	 *   <li>updates hyperlink information if the editor input is <code>IFileEditorInput</code></li>
	 * </ol>
	 */
	protected void update(){
		outline.update();
		updateFolding();
		
		if(getEditorInput() instanceof IFileEditorInput){
			doValidate();
			
			IFileEditorInput input = (IFileEditorInput)getEditorInput();
			JavaScriptConfiguration config = (JavaScriptConfiguration)getSourceViewerConfiguration();
			config.getAssistProcessor().update(input.getFile());
			hyperlinkDetector.update(input.getFile());
		}
	}
	
	/**
	 * Invokes <code>JavaScriptValidator</code> to validate editing source code.
	 * 
	 * @see JavaScriptValidator
	 */
	protected void doValidate(){
		try {
			ResourcesPlugin.getWorkspace().run(new IWorkspaceRunnable() {
				public void run(IProgressMonitor monitor) throws CoreException {
					try {
						IFileEditorInput input = (IFileEditorInput)getEditorInput();
						new JavaScriptValidator(input.getFile()).doValidate();
					} catch(Exception ex){
					}
				}
			},null);
		} catch(Exception ex){
			HTMLPlugin.logException(ex);
		}
	}
	
	@Override
  public void dispose() {
		if(getEditorInput() instanceof IFileEditorInput){
			try {
				ResourcesPlugin.getWorkspace().run(new IWorkspaceRunnable() {
					public void run(IProgressMonitor monitor) throws CoreException {
						try {
							IFileEditorInput input = (IFileEditorInput)getEditorInput();
							HTMLProjectParams params = new HTMLProjectParams(input.getFile().getProject());
							if(params.getRemoveMarkers()){
								input.getFile().deleteMarkers(IMarker.PROBLEM,false,0);
							}
						} catch(Exception ex){
						}
					}
				}, null);
			} catch(Exception ex){
			}
		}
		
		pairMatcher.dispose();
		super.dispose();
	}
	
	@Override
  public void doSaveAs() {
		super.doSaveAs();
		update();
	}
	
	@Override
  protected boolean affectsTextPresentation(PropertyChangeEvent event) {
		return super.affectsTextPresentation(event) || colorProvider.affectsTextPresentation(event);
	}
	
	@Override
  protected void handlePreferenceStoreChanged(PropertyChangeEvent event){
		colorProvider.handlePreferenceStoreChanged(event);
//		updateAssistProperties(event);
		
		String key = event.getProperty();
		if(key.equals(HTMLPlugin.PREF_PAIR_CHAR)){
			boolean enable = ((Boolean)event.getNewValue()).booleanValue();
			pairMatcher.setEnable(enable);
		}
		
		super.handlePreferenceStoreChanged(event);
		softTabListener.preferenceChanged(event);
	}	
	
	@Override
  protected void createActions() {
	    super.createActions();
	    // Add a content assist action
	    IAction action = new ContentAssistAction(
	    		HTMLPlugin.getDefault().getResourceBundle(),"ContentAssistProposal", this);
	    action.setActionDefinitionId(ITextEditorActionDefinitionIds.CONTENT_ASSIST_PROPOSALS);
	    setAction("ContentAssistProposal", action);
	}
	
	@Override
  public Object getAdapter(Class adapter){
		if(IContentOutlinePage.class.equals(adapter)){
			return outline;
		}
		return super.getAdapter(adapter);
	}
	
	/**
	 * Update folding informations.
	 */
	private void updateFolding(){
		try {
			ProjectionViewer viewer = (ProjectionViewer)getSourceViewer();
			if(viewer==null){
				return;
			}
			ProjectionAnnotationModel model = viewer.getProjectionAnnotationModel();
			if(model==null){
				return;
			}
			
			IDocument doc = getDocumentProvider().getDocument(getEditorInput());
			String source = doc.get();
			
			ArrayList<FoldingInfo> list = new ArrayList<FoldingInfo>();
			Stack<FoldingInfo> stack = new Stack<FoldingInfo>();
			FoldingInfo prev = null;
			char quote = 0;
			boolean escape = false;
			
			for(int i=0;i<source.length();i++){
				char c = source.charAt(i);
				// skip string
				if(quote!=0 && escape==true){
					escape = false;
				} else if((prev==null || !prev.getType().equals("comment")) && (c=='"' || c=='\'')){
					if(quote==0){
						quote = c;
					} else if(quote == c){
						quote = 0;
					}
				} else if(quote!=0 && (c=='\\')){
					escape = true;
				} else if(quote!=0 && (c=='\n' || c=='\r')){
					quote = 0;
				// start comment
				} else if(c=='/' && source.length() > i+1 && quote==0){
					if(source.charAt(i+1)=='*'){
						prev = new FoldingInfo(i,-1,"comment");
						stack.push(prev);
						i++;
					}
				// end comment
				} else if(c=='*' && source.length() > i+1 && !stack.isEmpty() && quote==0){
					if(source.charAt(i+1)=='/' && prev.getType().equals("comment")){
						FoldingInfo info = stack.pop();
						if(doc.getLineOfOffset(info.getStart())!=doc.getLineOfOffset(i)){
							list.add(new FoldingInfo(info.getStart(), i+2 + FoldingInfo.countUpLineDelimiter(source, i+2), "comment"));
						}
						prev = stack.isEmpty() ? null : (FoldingInfo)stack.get(stack.size()-1);
						i++;
					}
				// open blace
				} else if(c=='{' && quote==0){
					if(prev==null || !prev.getType().equals("comment")){
						if(findFunction(source, i)){
							prev = new FoldingInfo(i, -1, "function");
						} else {
							prev = new FoldingInfo(i, -1, "blace");
						}
						stack.push(prev);
					}
				// close blace
				} else if(c=='}' && prev!=null && !prev.getType().equals("comment") && quote==0){
					FoldingInfo info = stack.pop();
					if(info.getType().equals("function") && doc.getLineOfOffset(info.getStart())!=doc.getLineOfOffset(i)){
						list.add(new FoldingInfo(info.getStart(), i+2 + FoldingInfo.countUpLineDelimiter(source, i+2), "function"));
					}
					prev = stack.isEmpty() ? null : (FoldingInfo)stack.get(stack.size()-1);
				}
			}
			
			FoldingInfo.applyModifiedAnnotations(model, list);
			
		} catch(Exception ex){
			HTMLPlugin.logException(ex);
		}
	}
	
	private boolean findFunction(String text, int pos){
		text = text.substring(0, pos);
		int index1 = text.lastIndexOf("function");
		int index2 = text.lastIndexOf("{");
		if(index1==-1){
			return false;
		} else if(index1 > index2){
			return true;
		} else {
			return false;
		}
	}
	
	/**
	 * The action to toggle comment out selection range.
	 */
	private class CommentAction extends Action {
		
		public CommentAction(){
			super(HTMLPlugin.getResourceString("JavaScriptEditor.CommentAction"));
			setEnabled(false);
			setAccelerator(SWT.CTRL | '/');
		}
		
		@Override
    public void run() {
			ITextSelection sel = (ITextSelection)getSelectionProvider().getSelection();
			IDocument doc = getDocumentProvider().getDocument(getEditorInput());
			String text = sel.getText();
			text = text.replaceAll("[\r\n \t]+$", ""); // rtrim
			int length = text.length();
			try {
				if(text.startsWith("//")){
					text = text.replaceAll("(^|\r\n|\r|\n)//","$1");
				} else {
					text = text.replaceAll("(^|\r\n|\r|\n)","$1//");
				}
				doc.replace(sel.getOffset(), length, text);
			} catch (BadLocationException e) {
				HTMLPlugin.logException(e);
			}
		}
	}	
	
}
