/*******************************************************************************
 * Copyright (c) 2003, 2006 Subclipse project 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:
 *     Subclipse project committers - initial API and implementation
 ******************************************************************************/
package org.tigris.subversion.subclipse.ui.comments;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Observable;
import java.util.Observer;

import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.content.IContentTypeManager;
import org.eclipse.jface.dialogs.Dialog;
import org.eclipse.jface.dialogs.IDialogConstants;
import org.eclipse.jface.dialogs.IDialogSettings;
import org.eclipse.jface.dialogs.MessageDialogWithToggle;
import org.eclipse.jface.preference.IPreferenceStore;
import org.eclipse.jface.preference.PreferenceConverter;
import org.eclipse.jface.resource.DeviceResourceException;
import org.eclipse.jface.resource.JFaceColors;
import org.eclipse.jface.resource.JFaceResources;
import org.eclipse.jface.resource.LocalResourceManager;
import org.eclipse.jface.text.DefaultTextHover;
import org.eclipse.jface.text.Document;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.ITextHover;
import org.eclipse.jface.text.MarginPainter;
import org.eclipse.jface.text.Position;
import org.eclipse.jface.text.reconciler.DirtyRegion;
import org.eclipse.jface.text.reconciler.IReconciler;
import org.eclipse.jface.text.reconciler.IReconcilingStrategy;
import org.eclipse.jface.text.reconciler.MonoReconciler;
import org.eclipse.jface.text.source.Annotation;
import org.eclipse.jface.text.source.AnnotationModel;
import org.eclipse.jface.text.source.AnnotationPainter;
import org.eclipse.jface.text.source.AnnotationRulerColumn;
import org.eclipse.jface.text.source.CompositeRuler;
import org.eclipse.jface.text.source.IAnnotationAccess;
import org.eclipse.jface.text.source.IAnnotationModel;
import org.eclipse.jface.text.source.ISourceViewer;
import org.eclipse.jface.text.source.SourceViewer;
import org.eclipse.jface.text.source.SourceViewerConfiguration;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.StyledText;
import org.eclipse.swt.events.FocusEvent;
import org.eclipse.swt.events.FocusListener;
import org.eclipse.swt.events.ModifyEvent;
import org.eclipse.swt.events.ModifyListener;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.events.SelectionListener;
import org.eclipse.swt.events.TraverseEvent;
import org.eclipse.swt.events.TraverseListener;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Font;
import org.eclipse.swt.layout.FillLayout;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.widgets.Combo;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Link;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.team.internal.ui.SWTUtils;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.dialogs.PreferencesUtil;
import org.eclipse.ui.editors.text.EditorsUI;
import org.eclipse.ui.texteditor.AnnotationPreference;
import org.eclipse.ui.texteditor.DefaultMarkerAnnotationAccess;
import org.eclipse.ui.texteditor.spelling.ISpellingProblemCollector;
import org.eclipse.ui.texteditor.spelling.SpellingContext;
import org.eclipse.ui.texteditor.spelling.SpellingProblem;
import org.tigris.subversion.subclipse.core.SVNException;
import org.tigris.subversion.subclipse.core.util.Util;
import org.tigris.subversion.subclipse.ui.ISVNUIConstants;
import org.tigris.subversion.subclipse.ui.Policy;
import org.tigris.subversion.subclipse.ui.SVNUIPlugin;
import org.tigris.subversion.subclipse.ui.dialogs.DialogArea;
import org.tigris.subversion.subclipse.ui.settings.CommentProperties;

/**
 * This area provides the widgets for providing the SVN commit comment
 */
public class CommitCommentArea extends DialogArea {
	private boolean showLabel = true;;
    
    public static final String SPELLING_ERROR = "spelling.error"; //$NON-NLS-1$
    

    private class TextBox implements ModifyListener, TraverseListener, FocusListener, Observer {
        
        private final StyledText fTextField; // updated only by modify events
        private final String fMessage;
        
        private String fText;
		private LocalResourceManager fResources;
        
        public TextBox(Composite composite, String message, String initialText) {
            
            fMessage= message;
            fText= initialText;
            // Create a resource manager for the composite so it gets automatically disposed
            fResources= new LocalResourceManager(JFaceResources.getResources(), composite);
            
            AnnotationModel annotationModel = new AnnotationModel();
            IAnnotationAccess annotationAccess = new DefaultMarkerAnnotationAccess();
            
            AnnotationRulerColumn annotationRuler = new AnnotationRulerColumn(annotationModel, 16, annotationAccess);

            CompositeRuler compositeRuler = new CompositeRuler();
            compositeRuler.setModel(annotationModel);
            compositeRuler.addDecorator(0, annotationRuler);

            Composite cc = new Composite(composite, SWT.BORDER);
            cc.setLayout(new FillLayout());
            cc.setLayoutData(new GridData(GridData.FILL_BOTH));
            
            SourceViewer sourceViewer = new SourceViewer(cc, compositeRuler, null, true,
                SWT.MULTI | SWT.V_SCROLL | SWT.H_SCROLL);
            
            if (modifyListener != null) {
            	sourceViewer.getTextWidget().addModifyListener(modifyListener);
            }

            // TODO should be done in the source viewer configuration
            Font commentFont = PlatformUI.getWorkbench().getThemeManager().getCurrentTheme().getFontRegistry().get(ISVNUIConstants.SVN_COMMENT_FONT);
            if (commentFont != null) sourceViewer.getTextWidget().setFont(commentFont);
            
    		int widthMarker = 0;
    		if (commentProperties != null) widthMarker = commentProperties.getLogWidthMarker();
    		
    		if (widthMarker > 0) {
    			MarginPainter marginPainter = new MarginPainter(sourceViewer);
                marginPainter.setMarginRulerColumn(widthMarker);
                marginPainter.setMarginRulerColor(Display.getCurrent().getSystemColor(SWT.COLOR_GRAY));
                sourceViewer.addPainter(marginPainter);
    		}
            
            
            sourceViewer.showAnnotations(false);
            sourceViewer.showAnnotationsOverview(false);

            if (isSpellingAnnotationEnabled()) {
	            // to paint the annotations
	            AnnotationPainter ap = new AnnotationPainter(sourceViewer, annotationAccess);
	            ap.addAnnotationType(SPELLING_ERROR);
	            ap.setAnnotationTypeColor(SPELLING_ERROR, getSpellingErrorColor(composite));
	
	            // this will draw the squiggles under the text
	            sourceViewer.addPainter(ap);
            }

            Document document = new Document(initialText);

            // NOTE: Configuration must be applied before the document is set in order for
            // Hyperlink coloring to work. (Presenter needs document object up front)
            sourceViewer.configure(new SourceViewerConfig(annotationModel, document));
            
            sourceViewer.setDocument(document, annotationModel);
            
            fTextField = sourceViewer.getTextWidget();
            
            fTextField.addTraverseListener(this);
            fTextField.addModifyListener(this);
            fTextField.addFocusListener(this);
            fTextField.setWordWrap(mustWrapWord());
        }

        private boolean mustWrapWord() {
			if (commentProperties != null && commentProperties.getLogWidthMarker() > 0)	{
				return false;
			}
			return true;
		}
	
        private boolean isSpellingAnnotationEnabled() {
			// Need to determine how to ask the proper question to the AnnotationPreferences
			return true;
		}

		private Color getSpellingErrorColor(Composite composite) {
			AnnotationPreference pref = EditorsUI
					.getAnnotationPreferenceLookup().getAnnotationPreference(
							"org.eclipse.ui.workbench.texteditor.spelling"); // $NON-NLS-1$
			String preferenceKey = pref.getColorPreferenceKey();
			try {
				return fResources.createColor(PreferenceConverter.getColor(EditorsUI.getPreferenceStore(), preferenceKey));
			} catch (DeviceResourceException e) {
				SVNUIPlugin.log(IStatus.ERROR, Policy.bind("internal"), e); //$NON-NLS-1$
				return JFaceColors.getErrorText(composite.getDisplay());
			}
		}
        
        public void modifyText(ModifyEvent e) {
            final String old = fText;
            fText = fTextField.getText();
            firePropertyChangeChange(COMMENT_MODIFIED, old, fText);
        }
        
        public void keyTraversed(TraverseEvent e) {
        	if (e.detail == SWT.TRAVERSE_RETURN && (e.stateMask & SWT.SHIFT) != 0) {
	       		e.doit = false;
	       		return;
        	}
            if (e.detail == SWT.TRAVERSE_RETURN && (e.stateMask & SWT.CTRL) != 0) {
                e.doit = false;
                firePropertyChangeChange(OK_REQUESTED, null, null);
            }
        }
        
        public void focusGained(FocusEvent e) {

            if (fText.length() > 0) 
                return;
            
            fTextField.removeModifyListener(this);
            try {
                fTextField.setText(fText);
            } finally {
                fTextField.addModifyListener(this);
            }
        }
        
        public void focusLost(FocusEvent e) {
            
            if (fText.length() > 0) 
                return;
            
            fTextField.removeModifyListener(this);
            try {
                fTextField.setText(fMessage);
                fTextField.selectAll();
            } finally {
                fTextField.addModifyListener(this);
            }
        }
        
        public void setEnabled(boolean enabled) {
            fTextField.setEnabled(enabled);
        }
        
        public void update(Observable o, Object arg) {
            if (arg instanceof String) {
                setText((String)arg); // triggers a modify event
                if (modifyListener != null) modifyListener.modifyText(null);
            }
        }
        
        public String getText() {
            return fText;
        }
        
        public int getCommentLength() {
        	if (fTextField == null) return 0;
        	if (fTextField.getText().equals(Policy.bind("CommitCommentArea_0"))) return 0; //$NON-NLS-1$
        	return fTextField.getText().trim().length();
        }
        
        private void setText(String text) {
            if (text.length() == 0) {
                fTextField.setText(fMessage);
                fTextField.selectAll();
            } else
                fTextField.setText(text);
        }

        public void setFocus() {
            fTextField.setFocus();
        }
    }
    
    public class SourceViewerConfig extends SourceViewerConfiguration {

		private CommentSpellingReconcileStrategy strategy;

		public SourceViewerConfig(AnnotationModel annotationModel,
				Document document) {
			strategy = new CommentSpellingReconcileStrategy(annotationModel);
			strategy.setDocument(document);
		}

		public IReconciler getReconciler(ISourceViewer sourceViewer) {
			MonoReconciler reconciler = new MonoReconciler(strategy, false);
			reconciler.setIsIncrementalReconciler(false);
			reconciler.setProgressMonitor(new NullProgressMonitor());
			reconciler.setDelay(200);
			return reconciler;
		}

		/* (non-Javadoc)
		 * @see org.eclipse.jface.text.source.SourceViewerConfiguration#getTextHover(org.eclipse.jface.text.source.ISourceViewer, java.lang.String)
		 */
		public ITextHover getTextHover(ISourceViewer sourceViewer, String contentType) {
			return new DefaultTextHover(sourceViewer);
		}

	}
    
    public class CommentSpellingReconcileStrategy implements IReconcilingStrategy {


      /** The document to operate on. */
      private IDocument fDocument;

      private SpellingContext fSpellingContext;

      private IAnnotationModel fAnnotationModel;


      public CommentSpellingReconcileStrategy(AnnotationModel annotationModel) {
        this.fAnnotationModel = annotationModel;
        fSpellingContext = new SpellingContext();
        fSpellingContext.setContentType(Platform.getContentTypeManager().getContentType(IContentTypeManager.CT_TEXT));
      }

      public void reconcile(DirtyRegion dirtyRegion, IRegion subRegion) {
        reconcile(subRegion);
      }

      public void reconcile(IRegion region) {
        SpellingProblemCollector collector = new SpellingProblemCollector(fAnnotationModel);
        EditorsUI.getSpellingService().check(fDocument, fSpellingContext, collector, null);
      }

      public void setDocument(IDocument document) {
        fDocument = document;
      }

      
      /**
       * Spelling problem collector that forwards {@link SpellingProblem}s as
       * {@link IProblem}s to the {@link org.eclipse.jdt.core.IProblemRequestor}.
       */
      private class SpellingProblemCollector implements ISpellingProblemCollector {

        /** Annotation model */
        private IAnnotationModel fAnnotationModel;

        /** Annotations to add <ErrorAnnotation, Position> */
        private Map fAddAnnotations;

        /**
         * Initializes this collector with the given annotation model.
         * 
         * @param annotationModel
         *          the annotation model
         */
        public SpellingProblemCollector(IAnnotationModel annotationModel) {
          fAnnotationModel = annotationModel;
        }

        /*
         * @see org.eclipse.ui.texteditor.spelling.ISpellingProblemCollector#accept(org.eclipse.ui.texteditor.spelling.SpellingProblem)
         */
        public void accept(SpellingProblem problem) {
          fAddAnnotations.put(new Annotation(SPELLING_ERROR, false, problem.getMessage()), 
              new Position(problem.getOffset(), problem.getLength()));
        }

        /*
         * @see org.eclipse.ui.texteditor.spelling.ISpellingProblemCollector#beginCollecting()
         */
        public void beginCollecting() {
          fAddAnnotations = new HashMap();
        }

        /*
         * @see org.eclipse.ui.texteditor.spelling.ISpellingProblemCollector#endCollecting()
         */
        public void endCollecting() {
          List removeAnnotations = new ArrayList();
          for(Iterator iter = fAnnotationModel.getAnnotationIterator(); iter.hasNext();) {
            Annotation annotation = (Annotation) iter.next();
            if(SPELLING_ERROR.equals(annotation.getType()))
              removeAnnotations.add(annotation);
          }

          for(Iterator iter = removeAnnotations.iterator(); iter.hasNext();)
            fAnnotationModel.removeAnnotation((Annotation) iter.next());
          for(Iterator iter = fAddAnnotations.keySet().iterator(); iter.hasNext();) {
            Annotation annotation = (Annotation) iter.next();
            fAnnotationModel.addAnnotation(annotation, (Position) fAddAnnotations.get(annotation));
          }

          fAddAnnotations = null;
        }
      }
      
    }


    private static class ComboBox extends Observable implements SelectionListener, FocusListener {
        
        private final String fMessage;
        private final String [] fComments;
        private String[] fCommentTemplates;
        private final Combo fCombo;
        
        
        public ComboBox(Composite composite, String message, String [] options,
                String[] commentTemplates) {
            
            fMessage= message;
            fComments= options;
            fCommentTemplates = commentTemplates;
            
            fCombo = new Combo(composite, SWT.READ_ONLY);
            fCombo.setLayoutData(SWTUtils.createHFillGridData());
            fCombo.setVisibleItemCount(20);
            
            // populate the previous comment list
            populateList();
            
            // We don't want to have an initial selection
            // (see bug 32078: http://bugs.eclipse.org/bugs/show_bug.cgi?id=32078)
            fCombo.addFocusListener(this);
            fCombo.addSelectionListener(this);
        }

		private void populateList() {
			fCombo.removeAll();
			
			fCombo.add(fMessage);
            for (int i = 0; i < fCommentTemplates.length; i++) {
                fCombo.add(Policy.bind("CommitCommentArea_6") + ": " + //$NON-NLS-1$
                		Util.flattenText(fCommentTemplates[i]));
            }
            for (int i = 0; i < fComments.length; i++) {
                fCombo.add(Util.flattenText(fComments[i]));
            }
            fCombo.setText(fMessage);
		}
        
        public void widgetSelected(SelectionEvent e) {
            int index = fCombo.getSelectionIndex();
            if (index > 0) {
                index--;
                setChanged();
                
                // map from combo box index to array index
                String message;
                if (index < fCommentTemplates.length) {
                	message = fCommentTemplates[index];
                } else {
                	message = fComments[index - fCommentTemplates.length];
                }
                notifyObservers(message);
            }
        }
        
        public void widgetDefaultSelected(SelectionEvent e) {
        }
        
        public void focusGained(FocusEvent e) {
        }
        
        /* (non-Javadoc)
         * @see org.eclipse.swt.events.FocusListener#focusLost(org.eclipse.swt.events.FocusEvent)
         */
        public void focusLost(FocusEvent e) {
            fCombo.removeSelectionListener(this);
            try {
                fCombo.setText(fMessage);
            } finally {
                fCombo.addSelectionListener(this);
            }
        }
        
        public void setEnabled(boolean enabled) {
            fCombo.setEnabled(enabled);
        }
        
        void setCommentTemplates(String[] templates) {
			fCommentTemplates = templates;
			populateList();
		}
    }
    
    private static final String EMPTY_MESSAGE= Policy.bind("CommitCommentArea_0"); 
    private static final String COMBO_MESSAGE= Policy.bind("CommitCommentArea_1");
    
    public static final String OK_REQUESTED = "OkRequested";//$NON-NLS-1$
    public static final String COMMENT_MODIFIED = "CommentModified";//$NON-NLS-1$
    
    private TextBox fTextBox;
    private ComboBox fComboBox;
    
    private String fProposedComment;
    private Composite fComposite;
	private String enterCommentMessage;
	
	private CommentProperties commentProperties;
	private ModifyListener modifyListener;
	
    /**
	 * Constructor for CommitCommentArea.
	 * @param parentDialog
	 * @param settings
	 */
	public CommitCommentArea(Dialog parentDialog, IDialogSettings settings) {
		super(parentDialog, settings);
	}
	
	public CommitCommentArea(Dialog parentDialog, IDialogSettings settings, CommentProperties commentProperties) {
		this(parentDialog, settings);
		this.commentProperties = commentProperties;
	}
	
	/**
	 * Constructor for CommitCommentArea.
	 * @param parentDialog
	 * @param settings
	 * @param enterCommentMessage
	 */
	public CommitCommentArea(Dialog parentDialog, IDialogSettings settings, String enterCommentMessage) {
		this(parentDialog, settings);
		this.enterCommentMessage = enterCommentMessage;
	}
	
	public CommitCommentArea(Dialog parentDialog, IDialogSettings settings, String enterCommentMessage, CommentProperties commentProperties) {
		this(parentDialog, settings, enterCommentMessage);
		this.commentProperties = commentProperties;
	}	

    
    public Control createArea(Composite parent) {
        fComposite = createGrabbingComposite(parent, 1);
        initializeDialogUnits(fComposite);
        
        if (showLabel) {
			Label label = new Label(fComposite, SWT.NULL);
			label.setLayoutData(new GridData());
			if (enterCommentMessage == null) label.setText(Policy.bind("ReleaseCommentDialog.enterComment")); //$NON-NLS-1$
			else label.setText(enterCommentMessage);
        }
		
        fTextBox= new TextBox(fComposite, EMPTY_MESSAGE, getInitialComment());
        
        final String [] comments = SVNUIPlugin.getPlugin().getRepositoryManager().getCommentsManager().getPreviousComments();
        final String[] commentTemplates = SVNUIPlugin.getPlugin().getRepositoryManager().getCommentsManager().getCommentTemplates();
        fComboBox= new ComboBox(fComposite, COMBO_MESSAGE, comments, commentTemplates);
        
        Link templatesPrefsLink = new Link(fComposite, 0);
        templatesPrefsLink.setText("<a href=\"configureTemplates\">Configure Comment Templates...</a>"); //$NON-NLS-1$
        templatesPrefsLink.addSelectionListener(new SelectionListener() {
			public void widgetDefaultSelected(SelectionEvent e) {
				openCommentTemplatesPreferencePage();
			}
		
			public void widgetSelected(SelectionEvent e) {
				openCommentTemplatesPreferencePage();
			}
		});
        
        fComboBox.addObserver(fTextBox);
        return fComposite;
    }
    
    void openCommentTemplatesPreferencePage() {
		PreferencesUtil.createPreferenceDialogOn(
				null,
				"org.tigris.subversion.subclipse.ui.CommentTemplatesPreferences", //$NON-NLS-1$
				new String[] { "org.tigris.subversion.subclipse.ui.CommentTemplatesPreferences" }, //$NON-NLS-1$
				null).open();
		fComboBox.setCommentTemplates(
				SVNUIPlugin.getPlugin().getRepositoryManager().getCommentsManager().getCommentTemplates());
	}

    public String getComment() {
    	return getComment(false);
    }
    
	public String getComment(boolean save) {
        final String comment= fTextBox.getText();
        if (comment == null)
            return ""; //$NON-NLS-1$
        if (save) addComment(comment);
        return comment;
    }
	
	public void addComment(String comment) {
		if (comment != null && comment.trim().length() > 0) SVNUIPlugin.getPlugin().getRepositoryManager().getCommentsManager().addComment(comment);
	}
    
    public String getCommentWithPrompt(Shell shell) {
        final String comment= getComment(false);
        if (comment.length() == 0) {
            final IPreferenceStore store= SVNUIPlugin.getPlugin().getPreferenceStore();
            final String value= store.getString(ISVNUIConstants.PREF_ALLOW_EMPTY_COMMIT_COMMENTS);
            
            if (MessageDialogWithToggle.NEVER.equals(value))
                return null;
            
            if (MessageDialogWithToggle.PROMPT.equals(value)) {
                
                final String title= Policy.bind("CommitCommentArea_2"); 
                final String message= Policy.bind("CommitCommentArea_3"); 
                final String toggleMessage= Policy.bind("CommitCommentArea_4"); 
                
                final MessageDialogWithToggle dialog= MessageDialogWithToggle.openYesNoQuestion(shell, title, message, toggleMessage, false, store, ISVNUIConstants.PREF_ALLOW_EMPTY_COMMIT_COMMENTS);
                if (dialog.getReturnCode() != IDialogConstants.YES_ID) {
                    fTextBox.setFocus();
                    return null;
                }
            }
        }
        return getComment(true);
    }

    public void setFocus() {
        if (fTextBox != null) {
            fTextBox.setFocus();
        }
    }
    
    public void setProposedComment(String proposedComment) {
    	if (proposedComment == null || proposedComment.length() == 0) {
    		this.fProposedComment = null;
    	} else {
    		this.fProposedComment = proposedComment;
    	}
    }
    
    public boolean hasCommitTemplate() {
        try {
            String commitTemplate = getCommitTemplate();
            return commitTemplate != null && commitTemplate.length() > 0;
        } catch (SVNException e) {
            SVNUIPlugin.log(e);
            return false;
        }
    }
    
    public void setEnabled(boolean enabled) {
        fTextBox.setEnabled(enabled);
        fComboBox.setEnabled(enabled);
    }
    
    public Composite getComposite() {
        return fComposite;
    }
    
    public int getCommentLength() {
    	if (fTextBox == null) return 0;
    	return fTextBox.getCommentLength();
    }
    
    protected void firePropertyChangeChange(String property, Object oldValue, Object newValue) {
        super.firePropertyChangeChange(property, oldValue, newValue);
    }
    
    private String getInitialComment() {
        if (fProposedComment != null)
            return fProposedComment;
        try {
            return getCommitTemplate();
        } catch (SVNException e) {
            SVNUIPlugin.log(e);
            return ""; //$NON-NLS-1$
        }
    }

    private String getCommitTemplate() throws SVNException {
		if ((commentProperties != null) && (commentProperties.getLogTemplate() != null)) {
		    return commentProperties.getLogTemplate();
		}
        
        return ""; //$NON-NLS-1$
    }
    
    public void setModifyListener(ModifyListener modifyListener) {
        this.modifyListener = modifyListener;
    }

	public void setShowLabel(boolean showLabel) {
		this.showLabel = showLabel;
	}
    
}
