/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

package org.netbeans.modules.refactoring.java.plugins;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.EventQueue;
import java.awt.event.ActionEvent;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.lang.ref.Reference;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.JComponent;
import javax.swing.JDialog;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.KeyStroke;
import javax.swing.SwingUtilities;
import javax.swing.border.EmptyBorder;
import javax.swing.event.CaretEvent;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.event.UndoableEditEvent;
import javax.swing.event.UndoableEditListener;
import javax.swing.text.AttributeSet;
import javax.swing.text.BadLocationException;
import javax.swing.text.Document;
import javax.swing.text.JTextComponent;
import javax.swing.text.StyleConstants;
import javax.swing.undo.AbstractUndoableEdit;
import javax.swing.undo.CannotRedoException;
import javax.swing.undo.CannotUndoException;
import javax.swing.undo.CompoundEdit;
import javax.swing.undo.UndoableEdit;
import org.netbeans.api.editor.mimelookup.MimeLookup;
import org.netbeans.api.editor.mimelookup.MimePath;
import org.netbeans.api.editor.mimelookup.MimeRegistration;
import org.netbeans.api.editor.mimelookup.MimeRegistrations;
import org.netbeans.api.editor.settings.AttributesUtilities;
import org.netbeans.api.editor.settings.EditorStyleConstants;
import org.netbeans.api.editor.settings.FontColorSettings;
import org.netbeans.api.progress.ProgressHandle;
import org.netbeans.api.progress.ProgressHandleFactory;
import org.netbeans.editor.BaseDocument;
import org.netbeans.editor.BaseKit;
import org.netbeans.editor.EditorUI;
import org.netbeans.editor.Utilities;
import org.netbeans.lib.editor.util.swing.MutablePositionRegion;
import org.netbeans.modules.refactoring.api.AbstractRefactoring;
import org.netbeans.modules.refactoring.api.Problem;
import org.netbeans.modules.refactoring.api.ProgressEvent;
import org.netbeans.modules.refactoring.api.ProgressListener;
import org.netbeans.modules.refactoring.api.RefactoringSession;
import static org.netbeans.modules.refactoring.java.ui.ContextAnalyzer.SHOW;
import org.netbeans.modules.refactoring.java.ui.InstantRefactoringUI;
import org.netbeans.modules.refactoring.java.ui.SyncDocumentRegion;
import org.netbeans.modules.refactoring.java.ui.UIUtilities;
import org.netbeans.modules.refactoring.java.ui.instant.CompletionLayout;
import org.netbeans.modules.refactoring.java.ui.instant.InstantOption;
import org.netbeans.modules.refactoring.spi.RefactoringElementsBag;
import org.netbeans.modules.refactoring.spi.RefactoringPlugin;
import org.netbeans.modules.refactoring.spi.RefactoringPluginFactory;
import org.netbeans.spi.editor.document.UndoableEditWrapper;
import org.netbeans.spi.editor.highlighting.support.OffsetsBag;
import org.netbeans.spi.editor.typinghooks.DeletedTextInterceptor;
import org.openide.DialogDescriptor;
import org.openide.DialogDisplayer;
import org.openide.loaders.DataObject;
import org.openide.text.CloneableEditorSupport;
import org.openide.util.Exceptions;
import org.openide.util.NbBundle;
import org.openide.util.RequestProcessor;
import org.openide.util.WeakSet;
import org.openide.util.lookup.ServiceProvider;
import org.openide.windows.TopComponent;

/**
 *
 * @author Ralph Benjamin Ruijs
 */
@NbBundle.Messages({"# {0} - Shortcut text", "INFO_PressAgain=Press {0} Again for Refactoring Dialog"})
public final class InstantRefactoringPerformer implements DocumentListener, KeyListener, ProgressListener, PropertyChangeListener {
    
    private static final Logger LOG = Logger.getLogger(InstantRefactoringPerformer.class.getName());
    private static final Set<InstantRefactoringPerformer> registry = Collections.synchronizedSet(new WeakSet<InstantRefactoringPerformer>());
    private CompletionLayout compl;

    private SyncDocumentRegion region;
    private int span;
    private Document doc;
    private JTextComponent target;
    private InstantRefactoringUI ui;
    
    private AttributeSet attribs = null;
    private AttributeSet attribsLeft = null;
    private AttributeSet attribsRight = null;
    private AttributeSet attribsMiddle = null;
    private AttributeSet attribsAll = null;

    private AttributeSet attribsSlave = null;
    private AttributeSet attribsSlaveLeft = null;
    private AttributeSet attribsSlaveRight = null;
    private AttributeSet attribsSlaveMiddle = null;
    private AttributeSet attribsSlaveAll = null;

    public InstantRefactoringPerformer(final JTextComponent target, int caretOffset, InstantRefactoringUI ui) {
        releaseAll();
        this.target = target;
        this.ui = ui;
        doc = target.getDocument();

        MutablePositionRegion mainRegion = null;
        List<MutablePositionRegion> regions = new ArrayList<>(ui.getRegions().size());

        for (MutablePositionRegion current : ui.getRegions()) {
            // TODO: type parameter name is represented as ident -> ignore surrounding <> in rename
            if (isIn(current, caretOffset)) {
                mainRegion = current;
            } else {
                regions.add(current);
            }
        }

        if (mainRegion == null) {
            throw new IllegalArgumentException("No highlight contains the caret.");
        }

        regions.add(0, mainRegion);

        region = new SyncDocumentRegion(doc, regions);

        if (doc instanceof BaseDocument) {
            BaseDocument bdoc = ((BaseDocument) doc);
            bdoc.addPostModificationDocumentListener(this);
            
            UndoableWrapper wrapper = MimeLookup.getLookup("text/x-java").lookup(UndoableWrapper.class);
            if(wrapper != null) {
                wrapper.setActive(true, this);
            }

            UndoableEdit undo = new CancelInstantRenameUndoableEdit(this);
            for (UndoableEditListener l : bdoc.getUndoableEditListeners()) {
                l.undoableEditHappened(new UndoableEditEvent(doc, undo));
            }
        }

        target.addKeyListener(this);

        target.putClientProperty(InstantRefactoringPerformer.class, this);
        target.putClientProperty("NetBeansEditor.navigateBoundaries", mainRegion); // NOI18N
	
        requestRepaint();
        
        target.select(mainRegion.getStartOffset(), mainRegion.getEndOffset());
        
        span = region.getFirstRegionLength();
        compl = new CompletionLayout(this);
        compl.setEditorComponent(target);
        final KeyStroke OKKS = ui.getKeyStroke();
        target.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(OKKS, OKActionKey);
        Action OKAction = new AbstractAction() {
            @Override
            public void actionPerformed(ActionEvent ae) {
                if(registry.contains(InstantRefactoringPerformer.this)) {
                    doFullRefactoring();
                    target.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).remove(OKKS);
                    target.getActionMap().remove(OKActionKey);
                } else {
                    target.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).remove(OKKS);
                    target.getActionMap().remove(OKActionKey);
                }
            }
        };
        target.getActionMap().put(OKActionKey, OKAction);
        final KeyStroke keyStroke = ui.getKeyStroke();
        compl.showCompletion(ui.getOptions(), caretOffset, Bundle.INFO_PressAgain(getKeyStrokeAsText(keyStroke)));
        
        registry.add(this);
    }
    private static final String OKActionKey = "OK";
    
    private void doFullRefactoring() {
        releaseAll();
        final TopComponent activetc = TopComponent.getRegistry().getActivated();
        final Runnable task = new Runnable() {

            @Override
            public void run() {
                SHOW.show(ui.getRefactoringUI(), activetc);
            }
        };
        if(!EventQueue.isDispatchThread()) {
            SwingUtilities.invokeLater(new Runnable() {
                
                @Override
                public void run() {
                    UIUtilities.runWhenScanFinished(task, "Instant Refactoring");
                }
            });
        } else {
            UIUtilities.runWhenScanFinished(task, "Instant Refactoring");
        }
    }

    private static void releaseAll() {
        Iterator<InstantRefactoringPerformer> it = registry.iterator();
        while(it.hasNext()) {
            it.next().release(true);
            it.remove();
        }
    }
    
    private static String getKeyStrokeAsText(KeyStroke keyStroke) {
        int modifiers = keyStroke.getModifiers();
        StringBuilder sb = new StringBuilder();
        sb.append('\'');
        if ((modifiers & InputEvent.CTRL_DOWN_MASK) > 0) {
            sb.append("Ctrl+"); //NOI18N
        }
        if ((modifiers & InputEvent.ALT_DOWN_MASK) > 0) {
            sb.append("Alt+"); //NOI18N
        }
        if ((modifiers & InputEvent.SHIFT_DOWN_MASK) > 0) {
            sb.append("Shift+"); //NOI18N
        }
        if ((modifiers & InputEvent.META_DOWN_MASK) > 0) {
            sb.append("Meta+"); //NOI18N
        }
        if (keyStroke.getKeyCode() != KeyEvent.VK_SHIFT
                && keyStroke.getKeyCode() != KeyEvent.VK_CONTROL
                && keyStroke.getKeyCode() != KeyEvent.VK_META
                && keyStroke.getKeyCode() != KeyEvent.VK_ALT
                && keyStroke.getKeyCode() != KeyEvent.VK_ALT_GRAPH) {
            sb.append(org.openide.util.Utilities.keyToString(
                    KeyStroke.getKeyStroke(keyStroke.getKeyCode(), 0)
            ));
        }
        sb.append('\'');
        return sb.toString();
    }
    
    private static boolean isIn(MutablePositionRegion region, int caretOffset) {
	return region.getStartOffset() <= caretOffset && caretOffset <= region.getEndOffset();
    }
    
    private volatile boolean inSync;
    
    @Override
    public synchronized void insertUpdate(DocumentEvent e) {
	if (inSync)
	    return ;
	
        //check for modifications outside the first region:
        if (e.getOffset() < region.getFirstRegionStartOffset() || (e.getOffset() + e.getLength()) > region.getFirstRegionEndOffset()) {
            release(true);
            return;
        }
        
        inSync = true;
        region.sync();
        span = region.getFirstRegionLength();
        ui.updateInput(getRegionText(0));
        inSync = false;
        
        requestRepaint();
    }
    
    private String getRegionText(int regionIndex) {
        try {
            MutablePositionRegion r = region.getRegion(regionIndex);
            int offset = r.getStartOffset();
            int length = r.getEndOffset() - offset;
            return doc.getText(offset, length);
        } catch (BadLocationException e) {
            Exceptions.printStackTrace(e);
            return null;
        }
    }

    @Override
    public synchronized void removeUpdate(DocumentEvent e) {
	if (inSync)
	    return ;
	
        if (e.getLength() == 1) {
            if ((e.getOffset() < region.getFirstRegionStartOffset() || e.getOffset() > region.getFirstRegionEndOffset())) {
                release(true);
                return;
            }

            if (e.getOffset() == region.getFirstRegionStartOffset() && region.getFirstRegionLength() > 0 && region.getFirstRegionLength() == span) {
                if (LOG.isLoggable(Level.FINE)) {
                    LOG.log(Level.FINE, "e.getOffset()={0}", e.getOffset ());
                    LOG.log(Level.FINE, "region.getFirstRegionStartOffset()={0}", region.getFirstRegionStartOffset ());
                    LOG.log(Level.FINE, "region.getFirstRegionEndOffset()={0}", region.getFirstRegionEndOffset ());
                    LOG.log(Level.FINE, "span= {0}", span);
                }
                release(true);
                return;
            }
            
            if (e.getOffset() == region.getFirstRegionEndOffset() && region.getFirstRegionLength() > 0 && region.getFirstRegionLength() == span) {
                if (LOG.isLoggable(Level.FINE)) {
                    LOG.log(Level.FINE, "e.getOffset()={0}", e.getOffset ());
                    LOG.log(Level.FINE, "region.getFirstRegionStartOffset()={0}", region.getFirstRegionStartOffset ());
                    LOG.log(Level.FINE, "region.getFirstRegionEndOffset()={0}", region.getFirstRegionEndOffset ());
                    LOG.log(Level.FINE, "span= {0}", span);
                }

                release(true);
                return;
            }
            if (e.getOffset() == region.getFirstRegionEndOffset() && e.getOffset() == region.getFirstRegionStartOffset() && region.getFirstRegionLength() == 0 && region.getFirstRegionLength() == span) {
                if (LOG.isLoggable(Level.FINE)) {
                    LOG.log(Level.FINE, "e.getOffset()={0}", e.getOffset ());
                    LOG.log(Level.FINE, "region.getFirstRegionStartOffset()={0}", region.getFirstRegionStartOffset ());
                    LOG.log(Level.FINE, "region.getFirstRegionEndOffset()={0}", region.getFirstRegionEndOffset ());
                    LOG.log(Level.FINE, "span= {0}", span);
                }
                
               
                release(true);
                return;
            }
        } else {
            //selection/multiple characters removed:
            int removeSpan = e.getLength() + region.getFirstRegionLength();
            
            if (span < removeSpan) {
                release(true);
                return;
            }
        }
        
        //#89997: do not sync the regions for the "remove" part of replace selection,
        //as the consequent insert may use incorrect offset, and the regions will be synced
        //after the insert anyway.
        EditorUI editorUI = Utilities.getEditorUI(target);
        Boolean ovr = (Boolean) editorUI.getProperty(EditorUI.OVERWRITE_MODE_PROPERTY);
        if (doc.getProperty(BaseKit.DOC_REPLACE_SELECTION_PROPERTY) != null || (ovr != null && ovr == true)) {
            return ;
        }
        
        inSync = true;
        region.sync();
        span = region.getFirstRegionLength();
        inSync = false;
        
        requestRepaint();
    }

    @Override
    public void changedUpdate(DocumentEvent e) {
    }

    public void caretUpdate(CaretEvent e) {
    }

    @Override
    public void keyTyped(KeyEvent e) {
    }

    @Override
    public synchronized void keyPressed(KeyEvent e) {
        if (   (e.getKeyCode() == KeyEvent.VK_ESCAPE && e.getModifiers() == 0) ||
               (e.getKeyCode() == KeyEvent.VK_ENTER  && e.getModifiers() == 0)
        ) {
            release(e.getKeyCode() == KeyEvent.VK_ESCAPE);
            e.consume();
        }
    }

    @Override
    public void keyReleased(KeyEvent e) {
    }

    private synchronized void release(boolean cancel) {
        if(compl != null) {
            // should have been released
            compl.hideCompletion();
            compl = null;
        }
        if (target == null) {
            //already released
            return ;
        }
        
        UndoableWrapper wrapper = MimeLookup.getLookup("text/x-java").lookup(UndoableWrapper.class);
        if(wrapper != null) {
            wrapper.setActive(false, null);
            wrapper.close(true); // TODO: Simple changes do not need to go though full refactoring UI
        }
        target.putClientProperty("NetBeansEditor.navigateBoundaries", null); // NOI18N
        target.putClientProperty(InstantRefactoringPerformer.class, null);
        if (doc instanceof BaseDocument) {
            ((BaseDocument) doc).removePostModificationDocumentListener(this);
        }
        target.removeKeyListener(this);
        target.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).remove(ui.getKeyStroke());
        target.getActionMap().remove(OKActionKey);
        target = null;

        region = null;
        attribs = null;
        
        requestRepaint();

        doc = null;
        if (!cancel) {
            final DialogDescriptor descriptor = new DialogDescriptor(new JPanel(), ui.getName(), true, new Object[]{DialogDescriptor.CANCEL_OPTION}, null, 0, null, null);
            dialog = (JDialog) DialogDisplayer.getDefault().createDialog(descriptor);
            RP.post(new Runnable() {

                @Override
                public void run() {
                    dialog.setVisible(true);
                    if(ui != null) {
                        ui.getRefactoring().cancelRequest();
                    }
                    dialog.dispose();
                    dialog = null;
                }
            });
            RP.post(new Runnable() {
                @Override
                public void run() {
                    RefactoringSession session = RefactoringSession.create(ui.getName());
                    session.addProgressListener(InstantRefactoringPerformer.this);
                    ui.getRefactoring().addProgressListener(InstantRefactoringPerformer.this);
                    ui.getRefactoring().prepare(session);
                    if(dialog != null && dialog.isVisible()) {
                        dialog.getContentPane().setEnabled(false);
                        try {
                            session.doRefactoring(true);
                        } finally {
                            session.removeProgressListener(InstantRefactoringPerformer.this);
                            ui.getRefactoring().removeProgressListener(InstantRefactoringPerformer.this);
                            ui = null;
                            dialog.setVisible(false);
                        }
                    }
                }
            });
            
        }
    }

    private static final RequestProcessor RP = new RequestProcessor(InstantRefactoringPerformer.class.getName(), 2, false, false);
    
    private void requestRepaint() {
        if (region == null) {
            OffsetsBag bag = getHighlightsBag(doc);
            bag.clear();
        } else {
            // Compute attributes
            if (attribs == null) {
                // read the attributes for the master region
                attribs = getSyncedTextBlocksHighlight("synchronized-text-blocks-ext"); //NOI18N
                Color foreground = (Color) attribs.getAttribute(StyleConstants.Foreground);
                Color background = (Color) attribs.getAttribute(StyleConstants.Background);
                attribsLeft = createAttribs(
                        StyleConstants.Background, background,
                        EditorStyleConstants.LeftBorderLineColor, foreground, 
                        EditorStyleConstants.TopBorderLineColor, foreground, 
                        EditorStyleConstants.BottomBorderLineColor, foreground
                );
                attribsRight = createAttribs(
                        StyleConstants.Background, background,
                        EditorStyleConstants.RightBorderLineColor, foreground, 
                        EditorStyleConstants.TopBorderLineColor, foreground, 
                        EditorStyleConstants.BottomBorderLineColor, foreground
                );
                attribsMiddle = createAttribs(
                        StyleConstants.Background, background,
                        EditorStyleConstants.TopBorderLineColor, foreground, 
                        EditorStyleConstants.BottomBorderLineColor, foreground
                );
                attribsAll = createAttribs(
                        StyleConstants.Background, background,
                        EditorStyleConstants.LeftBorderLineColor, foreground, 
                        EditorStyleConstants.RightBorderLineColor, foreground,
                        EditorStyleConstants.TopBorderLineColor, foreground, 
                        EditorStyleConstants.BottomBorderLineColor, foreground
                );

                // read the attributes for the slave regions
                attribsSlave = getSyncedTextBlocksHighlight("synchronized-text-blocks-ext-slave"); //NOI18N
                Color slaveForeground = (Color) attribsSlave.getAttribute(StyleConstants.Foreground);
                Color slaveBackground = (Color) attribsSlave.getAttribute(StyleConstants.Background);
                attribsSlaveLeft = createAttribs(
                        StyleConstants.Background, slaveBackground,
                        EditorStyleConstants.LeftBorderLineColor, slaveForeground, 
                        EditorStyleConstants.TopBorderLineColor, slaveForeground, 
                        EditorStyleConstants.BottomBorderLineColor, slaveForeground
                );
                attribsSlaveRight = createAttribs(
                        StyleConstants.Background, slaveBackground,
                        EditorStyleConstants.RightBorderLineColor, slaveForeground, 
                        EditorStyleConstants.TopBorderLineColor, slaveForeground, 
                        EditorStyleConstants.BottomBorderLineColor, slaveForeground
                );
                attribsSlaveMiddle = createAttribs(
                        StyleConstants.Background, slaveBackground,
                        EditorStyleConstants.TopBorderLineColor, slaveForeground, 
                        EditorStyleConstants.BottomBorderLineColor, slaveForeground
                );
                attribsSlaveAll = createAttribs(
                        StyleConstants.Background, slaveBackground,
                        EditorStyleConstants.LeftBorderLineColor, slaveForeground, 
                        EditorStyleConstants.RightBorderLineColor, slaveForeground,
                        EditorStyleConstants.TopBorderLineColor, slaveForeground, 
                        EditorStyleConstants.BottomBorderLineColor, slaveForeground
                );
            }
            
            OffsetsBag nue = new OffsetsBag(doc);
            for(int i = 0; i < region.getRegionCount(); i++) {
                int startOffset = region.getRegion(i).getStartOffset();
                int endOffset = region.getRegion(i).getEndOffset();
                int size = region.getRegion(i).getLength();
                if (size == 1) {
                    nue.addHighlight(startOffset, endOffset, i == 0 ? attribsAll : attribsSlaveAll);
                } else if (size > 1) {
                    nue.addHighlight(startOffset, startOffset + 1, i == 0 ? attribsLeft : attribsSlaveLeft);
                    nue.addHighlight(endOffset - 1, endOffset, i == 0 ? attribsRight : attribsSlaveRight);
                    if (size > 2) {
                        nue.addHighlight(startOffset + 1, endOffset - 1, i == 0 ? attribsMiddle : attribsSlaveMiddle);
                    }
                }
            }
            
            OffsetsBag bag = getHighlightsBag(doc);
            bag.setHighlights(nue);
        }
    }
    
//    private static final AttributeSet defaultSyncedTextBlocksHighlight = AttributesUtilities.createImmutable(StyleConstants.Background, new Color(138, 191, 236));
    private static final AttributeSet defaultSyncedTextBlocksHighlight = AttributesUtilities.createImmutable(StyleConstants.Foreground, Color.red);
    
    private static AttributeSet getSyncedTextBlocksHighlight(String name) {
        FontColorSettings fcs = MimeLookup.getLookup(MimePath.EMPTY).lookup(FontColorSettings.class);
        AttributeSet as = fcs != null ? fcs.getFontColors(name) : null;
        return as == null ? defaultSyncedTextBlocksHighlight : as;
    }
    
    private static AttributeSet createAttribs(Object... keyValuePairs) {
        assert keyValuePairs.length % 2 == 0 : "There must be even number of prameters. " +
            "They are key-value pairs of attributes that will be inserted into the set.";

        List<Object> list = new ArrayList<Object>();
        
        for(int i = keyValuePairs.length / 2 - 1; i >= 0 ; i--) {
            Object attrKey = keyValuePairs[2 * i];
            Object attrValue = keyValuePairs[2 * i + 1];

            if (attrKey != null && attrValue != null) {
                list.add(attrKey);
                list.add(attrValue);
            }
        }
        
        return AttributesUtilities.createImmutable(list.toArray());
    }
    
    public static OffsetsBag getHighlightsBag(Document doc) {
        OffsetsBag bag = (OffsetsBag) doc.getProperty(InstantRefactoringPerformer.class);
        
        if (bag == null) {
            doc.putProperty(InstantRefactoringPerformer.class, bag = new OffsetsBag(doc));
            
            Object stream = doc.getProperty(Document.StreamDescriptionProperty);
            
            if (stream instanceof DataObject) {
                Logger.getLogger("TIMER").log(Level.FINE, "Instant Rename Highlights Bag", new Object[] {((DataObject) stream).getPrimaryFile(), bag}); //NOI18N
            }
        }
        
        return bag;
    }
    
    private JDialog dialog;
    private ProgressBar progressBar;
    private ProgressHandle progressHandle;
    private boolean isIndeterminate;

    /**
     * Implementation of ProgressListener.start method. Displays progress bar
     * and sets progress label and progress bar bounds.
     *
     * @param event Event object.
     */
    @Override
    @NbBundle.Messages({"LBL_ParametersCheck=Checking parameters...", "LBL_Usages=Usages", "LBL_Prepare=Prepare", "LBL_PreCheck=Initializing data..."})
    public void start(final ProgressEvent event) {
        SwingUtilities.invokeLater(new Runnable() {
            
            @Override
            public void run() {
                if (progressBar != null && progressBar.isVisible()) {
                    LOG.log(Level.INFO, "{0} called start multiple times", event.getSource());
                    stop(event);
                }

                progressBar = ProgressBar.create(progressHandle = ProgressHandleFactory.createHandle("")); //NOI18N
                if (event.getCount() == -1) {
                    isIndeterminate = true;
                    progressHandle.start();
                    progressHandle.switchToIndeterminate();
                } else {
                    isIndeterminate = false;
                    progressHandle.start(event.getCount());
                }
                String text;
                switch (event.getOperationType()) {
                    case AbstractRefactoring.PARAMETERS_CHECK:
                        text = NbBundle.getMessage(InstantRefactoringPerformer.class, "LBL_ParametersCheck");
                        break;
                    case AbstractRefactoring.PREPARE:
                        text = NbBundle.getMessage(InstantRefactoringPerformer.class, "LBL_Prepare");
                        break;
                    case AbstractRefactoring.PRE_CHECK:
                        text = NbBundle.getMessage(InstantRefactoringPerformer.class, "LBL_PreCheck");
                        break;
                    default:
                        text = NbBundle.getMessage(InstantRefactoringPerformer.class, "LBL_Usages");
                        break;
                }
                progressBar.setString(text);
                dialog.getContentPane().add(progressBar);
                dialog.pack();
            }
        });
    }

    /**
     * Implementation of ProgressListener.step method. Increments progress bar
     * value by 1.
     *
     * @param event Event object.
     */
    @Override
    public void step(final ProgressEvent event) {
        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                try {
                    if (progressHandle == null) {
                        return;
                    }

                    if (isIndeterminate && event.getCount() > 0) {
                        progressHandle.switchToDeterminate(event.getCount());
                        isIndeterminate = false;
                    } else {
                        progressHandle.progress(isIndeterminate ? -2 : event.getCount());
                    }
                } catch (Throwable e) {
                    Exceptions.printStackTrace(e);
                }
            }
        });
    }

    /**
     * Implementation of ProgressListener.stop method. Sets progress bar value
     * to its maximum.
     *
     * @param event Event object.
     */
    @Override
    public void stop(ProgressEvent event) {
        Runnable run = new Runnable() {
            @Override
            public void run() {
                if (progressHandle == null) {
                    return;
                }
                progressHandle.finish();
//                dialog.setVisible(false);
                //progressPanel.validate();
                //setButtonsEnabled(true); 
                //validate();
                progressHandle = null;
            }
        };
        if (SwingUtilities.isEventDispatchThread()) {
            run.run();
        } else {
            SwingUtilities.invokeLater(run);
        }
    }

    @Override
    public void propertyChange(PropertyChangeEvent evt) {
        if(!evt.getOldValue().equals(evt.getNewValue()) && evt.getSource() instanceof InstantOption) {
            Set<MutablePositionRegion> newChangePoints = ui.optionChanged((InstantOption)evt.getSource());
            if(newChangePoints != null) {
                MutablePositionRegion mainRegion = region.getRegion(0);
                if(!newChangePoints.contains(mainRegion)) {
                    throw new IllegalArgumentException("MainRegion not part of the new change points.");
                }
                
                List<MutablePositionRegion> regions = new ArrayList<>(newChangePoints.size());

                for (MutablePositionRegion current : newChangePoints) {
                    if(current != mainRegion) {
                        regions.add(current);
                    }
                }

                regions.add(0, mainRegion);
                inSync = true;
                region.updateRegions(regions);
                inSync = false;
                requestRepaint();
            }
        }
    }
    
    private static class ProgressBar extends JPanel {

        private JLabel label;

        private static ProgressBar create(ProgressHandle handle) {
            ProgressBar instance = new ProgressBar();
            instance.setLayout(new BorderLayout());
            instance.label = new JLabel(" "); //NOI18N
            instance.label.setBorder(new EmptyBorder(0, 0, 2, 0));
            instance.add(instance.label, BorderLayout.NORTH);
            JComponent progress = ProgressHandleFactory.createProgressComponent(handle);
            instance.add(progress, BorderLayout.CENTER);
            return instance;
        }

        public void setString(String value) {
            label.setText(value);
        }

        private ProgressBar() {
        }
    }
    
    private static class CancelInstantRenameUndoableEdit extends AbstractUndoableEdit {

        private final Reference<InstantRefactoringPerformer> performer;

        public CancelInstantRenameUndoableEdit(InstantRefactoringPerformer performer) {
            this.performer = new WeakReference<InstantRefactoringPerformer>(performer);
        }

        @Override public boolean isSignificant() {
            return false;
        }

        @Override public void undo() throws CannotUndoException {
            InstantRefactoringPerformer perf = performer.get();

            if (perf != null) {
                perf.release(true);
            }
        }
    }
    
    @MimeRegistration(mimeType = "text/x-java", service = UndoableEditWrapper.class)
    public static class UndoableWrapper implements UndoableEditWrapper {

        private AtomicBoolean active = new AtomicBoolean(false);
        private Map<BaseDocument, UndoableEditDelegate> docToFirst = new HashMap<>();
        private InstantRefactoringPerformer performer;

        public UndoableWrapper() {
        }

        @Override
        public UndoableEdit wrap(UndoableEdit ed, Document doc) {
            if (!active.get()) {
                return ed;
            }
            if (doc.getProperty(BaseDocument.StreamDescriptionProperty) == null) {
                //no dataobject
                return ed;
            }
            UndoableEditDelegate current = new UndoableEditDelegate(ed, (BaseDocument) doc, performer);
            UndoableEditDelegate first = docToFirst.get(doc);
            if (first == null) {
                docToFirst.put((BaseDocument) doc, current);
            }
            return current;
        }

        public void setActive(boolean b, InstantRefactoringPerformer performer) {
            this.performer = performer;
            active.set(b);
        }

        public void close(boolean cancel) {
            for (UndoableEditDelegate first : docToFirst.values()) {
                first.end();
                if (cancel) {
                    first.undo();
                    first.die();
                }
            }
            docToFirst.clear();
        }

        public class UndoableEditDelegate implements UndoableEdit {

            private CloneableEditorSupport ces;
            private UndoableEdit delegate;
            private CompoundEdit inner;
            private InstantRefactoringPerformer performer;

            private UndoableEditDelegate(UndoableEdit ed, BaseDocument doc, InstantRefactoringPerformer performer) {
                DataObject dob = (DataObject) doc.getProperty(BaseDocument.StreamDescriptionProperty);
                this.ces = dob.getLookup().lookup(CloneableEditorSupport.class);
                this.inner = new CompoundEdit();
                this.inner.addEdit(ed);
                this.delegate = ed;
                this.performer = performer;
            }

            @Override
            public void undo() throws CannotUndoException {
//                JTextComponent focusedComponent = EditorRegistry.focusedComponent();
//                if (focusedComponent != null) {
//                    if (focusedComponent.getDocument() == ces.getDocument()) {
//                        //call global undo only for focused component
//                        undoManager.undo(session);
//                    }
//                }
                //delegate.undo();
                inner.undo();
            }

            @Override
            public boolean canUndo() {
//                return delegate.canUndo();
                return inner.canUndo();
            }

            @Override
            public void redo() throws CannotRedoException {
//                JTextComponent focusedComponent = EditorRegistry.focusedComponent();
//                if (focusedComponent != null) {
//                    if (focusedComponent.getDocument() == ces.getDocument()) {
//                        //call global undo only for focused component
//                        undoManager.redo(session);
//                    }
//                }
                //delegate.redo();
                inner.redo();
            }

            @Override
            public boolean canRedo() {
                //return delegate.canRedo();
                return inner.canRedo();
            }

            @Override
            public void die() {
                //delegate.die();
                inner.die();
            }

            @Override
            public boolean addEdit(UndoableEdit ue) {
                if (ue instanceof List) {
                    List<UndoableEdit> listEdit = (List<UndoableEdit>) ue;
                    UndoableEdit topEdit = listEdit.get(listEdit.size() - 1);
                    // Check that there's only original document's edit and the wrapping refactoring edit
                    boolean refatoringEditOnly = listEdit.size() == 2;
                    if (refatoringEditOnly && topEdit instanceof UndoableEditDelegate) {
                        inner.addEdit(listEdit.get(0));
                        return true;
                    }
                    return false;
                }
                return false;
            }

            public UndoableEdit unwrap() {
                return delegate;
            }

            @Override
            public boolean replaceEdit(UndoableEdit ue) {
                return inner.replaceEdit(ue);
                //return delegate.replaceEdit(ue);
            }

            @Override
            public boolean isSignificant() {
                return inner.isSignificant();
                //return delegate.isSignificant();
            }

            @Override
            public String getPresentationName() {
                return "Rename";
            }

            @Override
            public String getUndoPresentationName() {
                return "Undo Rename";
            }

            @Override
            public String getRedoPresentationName() {
                return "Redo Rename";
            }

            private void end() {
                inner.end();
            }
        }
    }
    
    @ServiceProvider(service=RefactoringPluginFactory.class, position=95)
    public static class AllRefactoringsPluginFactory implements RefactoringPluginFactory {

        @Override
        public RefactoringPlugin createInstance(AbstractRefactoring refactoring) {
            return new RefactoringPluginImpl();
        }

        private static final class RefactoringPluginImpl implements RefactoringPlugin {

            @Override
            public Problem preCheck() {
                return null;
            }

            @Override
            public Problem checkParameters() {
                return null;
            }

            @Override
            public Problem fastCheckParameters() {
                return null;
            }

            @Override
            public void cancelRequest() {}

            @Override
            public Problem prepare(RefactoringElementsBag refactoringElements) {
                refactoringElements.getSession().addProgressListener(new ProgressListener() {
                    @Override
                    public void start(ProgressEvent event) {
                        final InstantRefactoringPerformer[] performers = registry.toArray(new InstantRefactoringPerformer[0]);
                        for (InstantRefactoringPerformer p : performers) {
                            p.inSync = true;
                        }
                        SwingUtilities.invokeLater(new Runnable() {
                            @Override
                            public void run() {
                                for (InstantRefactoringPerformer p : performers) {
                                    p.release(true);
                                }
                            }
                        });
                    }
                    @Override
                    public void step(ProgressEvent event) {}
                    @Override
                    public void stop(ProgressEvent event) {}
                });

                return null;
            }

        }

    }
    
    public static class RenameDeletedTextInterceptor implements DeletedTextInterceptor {
        
        @Override
        public boolean beforeRemove(DeletedTextInterceptor.Context context) throws BadLocationException {
            Object getObject = context.getComponent().getClientProperty(InstantRefactoringPerformer.class);
            if (getObject instanceof InstantRefactoringPerformer) {
                InstantRefactoringPerformer instantRenamePerformer = (InstantRefactoringPerformer)getObject;
                MutablePositionRegion region = instantRenamePerformer.region.getRegion(0);
                return ((context.isBackwardDelete() && region.getStartOffset() == context.getOffset()) || (!context.isBackwardDelete() && region.getEndOffset() == context.getOffset()));
            } else {
                return false;
            }
        }
        @Override
        public void remove(DeletedTextInterceptor.Context context) throws BadLocationException {            
        }

        @Override
        public void afterRemove(DeletedTextInterceptor.Context context) throws BadLocationException {
        }

        @Override
        public void cancelled(DeletedTextInterceptor.Context context) {
        }

        @MimeRegistrations({
            @MimeRegistration(mimeType = "text/x-java", service = DeletedTextInterceptor.Factory.class)
        })
        public static class Factory implements DeletedTextInterceptor.Factory {

            @Override
            public DeletedTextInterceptor createDeletedTextInterceptor(MimePath mimePath) {
                return new RenameDeletedTextInterceptor();
            }
        }
    }
}
