/*
 * 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.editor.search;

import java.awt.Insets;
import java.awt.Rectangle;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.lang.ref.WeakReference;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.WeakHashMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.prefs.Preferences;
import javax.swing.text.JTextComponent;
import javax.swing.text.BadLocationException;
import javax.swing.text.Caret;
import javax.swing.text.Document;
import javax.swing.text.PlainDocument;
import javax.swing.text.Position;
import org.netbeans.api.editor.EditorRegistry;
import org.netbeans.api.editor.mimelookup.MimeLookup;
import org.netbeans.api.editor.mimelookup.MimePath;
import org.netbeans.api.editor.settings.FontColorNames;
import org.netbeans.api.editor.settings.SimpleValueNames;
import org.netbeans.api.editor.NavigationHistory;
import org.netbeans.api.editor.caret.EditorCaret;
import org.netbeans.modules.editor.lib2.ComponentUtils;
import org.netbeans.modules.editor.lib2.DocUtils;
import org.netbeans.modules.editor.lib2.highlighting.BlockHighlighting;
import org.netbeans.modules.editor.lib2.highlighting.Factory;
import org.netbeans.modules.editor.search.DocumentFinder.FindReplaceResult;
import org.openide.util.Exceptions;
import org.openide.util.NbBundle;
import org.openide.util.RequestProcessor;

/**
* Find management
*
* @author Miloslav Metelka
* @version 1.00
*/

public final class EditorFindSupport {

    private static final Logger LOG = Logger.getLogger(EditorFindSupport.class.getName());
    
    /* Find properties.
    * They are read by FindSupport when its instance is being initialized.
    * FIND_WHAT: java.lang.String - search expression
    * FIND_REPLACE_BY: java.lang.String - replace string
    * FIND_HIGHLIGHT_SEARCH: java.lang.Boolean - highlight matching strings in text
    * FIND_INC_SEARCH: java.lang.Boolean - show matching strings immediately
    * FIND_BACKWARD_SEARCH: java.lang.Boolean - search in backward direction
    * FIND_WRAP_SEARCH: java.lang.Boolean - if end of doc reached, start from begin
    * FIND_MATCH_CASE: java.lang.Boolean - match case of letters
    * FIND_SMART_CASE: java.lang.Boolean - case insensitive search if FIND_MATCH_CASE
    *   is false and all letters of FIND_WHAT are small, case sensitive otherwise
    * FIND_WHOLE_WORDS: java.lang.Boolean - match only whole words
    * FIND_REG_EXP: java.lang.Boolean - use regular expressions in search expr
    * FIND_HISTORY: java.util.List - History of search expressions
    * FIND_HISTORY_SIZE: java.lang.Integer - Maximum size of the history
    * FIND_BLOCK_SEARCH: java.lang.Boolean - search in block
    * FIND_BLOCK_SEARCH_START: javax.swing.text.Position - start position of the block in block search
    * FIND_BLOCK_SEARCH_END: javax.swing.text.Position - end position of the block in block search
    * 
    */
    public static final String FIND_WHAT = "find-what"; // NOI18N
    public static final String FIND_REPLACE_WITH = "find-replace-with"; // NOI18N
    public static final String FIND_HIGHLIGHT_SEARCH = "find-highlight-search"; // NOI18N
    public static final String FIND_INC_SEARCH = "find-inc-search"; // NOI18N
    public static final String FIND_INC_SEARCH_DELAY = "find-inc-search-delay"; // NOI18N
    public static final String FIND_BACKWARD_SEARCH = "find-backward-search"; // NOI18N
    public static final String FIND_WRAP_SEARCH = "find-wrap-search"; // NOI18N
    public static final String FIND_MATCH_CASE = "find-match-case"; // NOI18N
    public static final String FIND_SMART_CASE = "find-smart-case"; // NOI18N
    public static final String FIND_PRESERVE_CASE = "find-preserve-case"; // NOI18N
    public static final String FIND_WHOLE_WORDS = "find-whole-words"; // NOI18N
    public static final String FIND_REG_EXP = "find-reg-exp"; // NOI18N
    public static final String FIND_HISTORY = "find-history"; // NOI18N
    public static final String FIND_HISTORY_SIZE = "find-history-size"; // NOI18N
    public static final String FIND_BLOCK_SEARCH = "find-block-search"; //NOI18N
    public static final String FIND_BLOCK_SEARCH_START = "find-block-search-start"; //NOI18N
    public static final String FIND_BLOCK_SEARCH_END = "find-block-search-end"; //NOI18N
    public static final String ADD_MULTICARET = "add-multi-caret"; //NOI18N

    private static final String FOUND_LOCALE = "find-found"; // NOI18N
    private static final String NOT_FOUND_LOCALE = "find-not-found"; // NOI18N
    private static final String WRAP_START_LOCALE = "find-wrap-start"; // NOI18N
    private static final String WRAP_END_LOCALE = "find-wrap-end"; // NOI18N
    private static final String WRAP_BLOCK_START_LOCALE = "find-block-wrap-start"; // NOI18N
    private static final String WRAP_BLOCK_END_LOCALE = "find-block-wrap-end"; // NOI18N
    private static final String ITEMS_REPLACED_LOCALE = "find-items-replaced"; // NOI18N
    /** It's public only to keep backwards compatibility of the FindSupport class. */
    public static final String REVERT_MAP = "revert-map"; // NOI18N

    /** It's public only to keep backwards compatibility of the FindSupport class. */
    public static final String FIND_HISTORY_PROP = "find-history-prop"; //NOI18N
    public static final String REPLACE_HISTORY_PROP = "replace-history-prop"; //NOI18N
    /** It's public only to keep backwards compatibility of the FindSupport class. */
    public static final String FIND_HISTORY_CHANGED_PROP = "find-history-changed-prop"; //NOI18N
    public static final String REPLACE_HISTORY_CHANGED_PROP = "replace-history-changed-prop"; //NOI18N
    
    /**
     * Default message 'importance' for messages from find and replace actions.
     * <br/>
     * Corresponds to StatusDisplayer.IMPORTANCE_FIND_OR_REPLACE.
     */
    private static final int IMPORTANCE_FIND_OR_REPLACE = 800;

    /** Shared instance of FindSupport class */
    private static EditorFindSupport findSupport;

    /** Find properties */
    private Map<String, Object> findProps;
    private WeakReference<JTextComponent> focusedTextComponent;
    private final RequestProcessor executor = new RequestProcessor(EditorFindSupport.class.getName(), 1);

    private final WeakHashMap<JTextComponent, Map<String, WeakReference<BlockHighlighting>>> comp2layer =
        new WeakHashMap<>();
    
    /** Support for firing change events */
    private final PropertyChangeSupport changeSupport = new PropertyChangeSupport(this);
    
    private SPW lastSelected;
    private List<SPW> historyList = new ArrayList<>();
    private List<RP> replaceList = new ArrayList<>();

    private String cachekey = "";
    private int[] cacheContent = new int[0];
    private static final int TIME_LIMIT = 2;
    
    private EditorFindSupport() {
    }

    /** Get shared instance of find support */
    public synchronized static EditorFindSupport getInstance() {
        if (findSupport == null) {
            findSupport = new EditorFindSupport();
        }
        return findSupport;
    }

    public Map<String, Object> createDefaultFindProperties() {
        HashMap<String, Object> props = new HashMap<>();
        
        props.put(FIND_WHAT, null);
        props.put(FIND_REPLACE_WITH, null);
        props.put(FIND_HIGHLIGHT_SEARCH, Boolean.TRUE);
        props.put(FIND_INC_SEARCH, Boolean.TRUE);
        props.put(FIND_BACKWARD_SEARCH, Boolean.FALSE);
        props.put(FIND_WRAP_SEARCH, Boolean.TRUE);
        props.put(FIND_MATCH_CASE, Boolean.FALSE);
        props.put(FIND_SMART_CASE, Boolean.FALSE);
        props.put(FIND_WHOLE_WORDS, Boolean.FALSE);
        props.put(FIND_REG_EXP, Boolean.FALSE);
        props.put(FIND_HISTORY, Integer.valueOf(30));
        props.put(FIND_PRESERVE_CASE, Boolean.FALSE);
        props.put(ADD_MULTICARET, Boolean.FALSE);

        return props;
    }

    private int getBlockEndOffset(){
        Position pos = (Position) getFindProperties().get(FIND_BLOCK_SEARCH_END);
        return (pos != null) ? pos.getOffset() : -1;
    }
    
    public Map<String, Object> getFindProperties() {
        if (findProps == null) {
            findProps = createDefaultFindProperties();
        }
        return findProps;
    }

    /** Get find property with specified name */
    public Object getFindProperty(String name) {
        return getFindProperties().get(name);
    }

    private Map<String, Object> getValidFindProperties(Map<String, Object> props) {
        return (props != null) ? props : getFindProperties();
    }

    /**
     * <p><b>IMPORTANT:</b> This method is public only for keeping backwards
     * compatibility of the {@link org.netbeans.editor.FindSupport} class.
     */
    public synchronized int[] getBlocks(final int[] blocks, final Document doc,
            int startOffset, int endOffset) throws BadLocationException {
        final Map<String, Object> props = getValidFindProperties(null);

        String newCacheKey = calculateCacheKey(doc, startOffset, endOffset, props);
        if (cachekey.equals(newCacheKey)) {
            return Arrays.copyOf(cacheContent, cacheContent.length);
        }

        boolean blockSearch = Boolean.TRUE.equals(props.get(FIND_BLOCK_SEARCH));
        Position blockSearchStartPos = (Position) props.get(FIND_BLOCK_SEARCH_START);
        Position blockSearchEndPos = (Position) props.get(FIND_BLOCK_SEARCH_END);

        if (blockSearch && blockSearchStartPos != null && blockSearchEndPos != null){
            if (endOffset >= blockSearchStartPos.getOffset() &&
                    startOffset <= blockSearchEndPos.getOffset())
            {
                startOffset = Math.max(blockSearchStartPos.getOffset(), startOffset);
                endOffset = Math.min(blockSearchEndPos.getOffset(), endOffset);
            } else {
                return blocks;
            }
        }

        final int so = startOffset;
        final int eo = endOffset;
        currentResult = null;
        try {
            executor.submit(new Runnable() {

                @Override
                public void run() {
                    try {
                        currentResult = DocumentFinder.findBlocks(doc, so, eo, props, blocks);
                        cacheContent = currentResult.getFoundPositions();
                    } catch (BadLocationException ble) {
                        cacheContent = Arrays.copyOf(blocks, blocks.length);
                        LOG.log(Level.INFO, ble.getMessage(), ble);
                    }
                    
                }
            }).get(TIME_LIMIT, TimeUnit.SECONDS);
        } catch (InterruptedException | ExecutionException | TimeoutException ex) {
            cacheContent = Arrays.copyOf(blocks, blocks.length);
            org.netbeans.editor.Utilities.setStatusBoldText(getFocusedTextComponent(), NbBundle.getMessage(EditorFindSupport.class, "slow-search"));
            LOG.log(Level.INFO, ex.getMessage(), ex);
        }
        if (currentResult != null && currentResult.hasErrorMsg()) {
            org.netbeans.editor.Utilities.setStatusBoldText(getFocusedTextComponent(), currentResult.getErrorMsg());
        }
        cachekey = newCacheKey;
        return Arrays.copyOf(cacheContent, cacheContent.length);
    }

    /** Set find property with specified name and fire change.
    */
    public void putFindProperty(String name, Object newValue) {
        Object oldValue = getFindProperty(name);
        if ((oldValue == null && newValue == null)
                || (oldValue != null && oldValue.equals(newValue))
           ) {
            return;
        }
        if (newValue != null) {
            getFindProperties().put(name, newValue);
        } else {
            getFindProperties().remove(name);
        }
        firePropertyChange(name, oldValue, newValue);
    }

    /**
     * Add/replace properties from some other map
     * to current find properties. If the added properties
     * are different than the original ones,
     * the property change is fired.
     */
    public void putFindProperties(Map<String, Object> propsToAdd) {
        if (getFindProperties() != propsToAdd) {
            getFindProperties().putAll(propsToAdd);
        }
        //highlight will not be updated on empty properties
        if (propsToAdd.get(FIND_WHAT) != null) {
            firePropertyChange(null, null, null);
        }
    }
    
    public void setFocusedTextComponent(JTextComponent component) { 
        focusedTextComponent = new WeakReference<>(component);
        firePropertyChange(null, null, null);
    }
    
    public JTextComponent getFocusedTextComponent() {
        JTextComponent jc = focusedTextComponent != null ? focusedTextComponent.get() : null;
        return (jc != null) ? jc : EditorRegistry.lastFocusedComponent();
    }
    
    public void setBlockSearchHighlight(int startSelection, int endSelection){
        JTextComponent comp = getFocusedTextComponent();
        BlockHighlighting layer = comp == null ? null : findLayer(comp, Factory.BLOCK_SEARCH_LAYER);

        if (layer != null) {
            
            if (startSelection >= 0 && endSelection >= 0 && startSelection < endSelection ) {
                layer.highlightBlock(startSelection, endSelection, FontColorNames.BLOCK_SEARCH_COLORING, true, true);
            } else {
                layer.highlightBlock(-1, -1, FontColorNames.BLOCK_SEARCH_COLORING, true, true);
            }            
        }
    }
    
    public boolean incSearch(Map<String, Object> props, int caretPos) {
        props = getValidFindProperties(props);
        
        Boolean b = (Boolean)props.get(FIND_INC_SEARCH);
        if (b != null && b.booleanValue()) { // inc search enabled
            JTextComponent comp = getFocusedTextComponent();
            
            if (comp != null) {
                b = (Boolean)props.get(FIND_BACKWARD_SEARCH);
                boolean back = (b != null && b.booleanValue());
                b = (Boolean)props.get(FIND_BLOCK_SEARCH);
                boolean blockSearch = (b != null && b.booleanValue());
                Position blockStartPos = (Position) props.get(FIND_BLOCK_SEARCH_START);
                int blockSearchStartOffset = (blockStartPos != null) ? blockStartPos.getOffset() : -1;
                
                Position endPos = (Position) props.get(FIND_BLOCK_SEARCH_END);
                int blockSearchEndOffset = (endPos != null) ? endPos.getOffset() : -1;
                int pos;
                int len = 0;
                try {
                    int start = (blockSearch && blockSearchStartOffset > -1) ? blockSearchStartOffset : 0;
                    int end = (blockSearch && blockSearchEndOffset > 0) ? blockSearchEndOffset : -1;
                    if (start > 0 && end == -1) {
                        return false;
                    }
                    int findRet[] = findInBlock(comp, caretPos, 
                        start, 
                        end, 
                        props, false);
                            
                    if (findRet == null) {
                        incSearchReset();
                        return false;
                    }
                    pos = findRet[0];
                    len = findRet.length > 1 ? findRet[1] - pos : 0;
                } catch (BadLocationException e) {
                    LOG.log(Level.WARNING, e.getMessage(), e);
                    return false;
                }
                
                if (pos >= 0) {
                    // Find the layer
                    BlockHighlighting layer = findLayer(comp, Factory.INC_SEARCH_LAYER);

                    if (len > 0) {
                        if (comp.getSelectionEnd() > comp.getSelectionStart()){
                            comp.select(caretPos, caretPos);
                        }
                        
                        if (layer != null) {
                            layer.highlightBlock(
                                pos,
                                pos + len,
                                blockSearch ? FontColorNames.INC_SEARCH_COLORING : FontColorNames.SELECTION_COLORING,
                                false,
                                false
                            );
                        }
                        Preferences prefs = MimeLookup.getLookup(MimePath.EMPTY).lookup(Preferences.class);
                        if (prefs.get(SimpleValueNames.EDITOR_SEARCH_TYPE, "default").equals("closing")) { // NOI18N
                            ensureVisible(comp, pos, pos);
                        } else {
                            selectText(comp, pos, pos + len, back);
                        }
                        return true;
                    }
                }
               
            }
        } else { // inc search not enabled
            incSearchReset();
        }
        return false;
    }

    public void incSearchReset() {
        // Find the layer
        JTextComponent comp = getFocusedTextComponent();
        BlockHighlighting layer = comp == null ? null : findLayer(comp, Factory.INC_SEARCH_LAYER);
        
        if (layer != null) {
            layer.highlightBlock(-1, -1, null, false, false);
        }
    }
    
    private boolean isBackSearch(Map<String, Object> props, boolean oppositeDir) {
        Boolean b = (Boolean)props.get(FIND_BACKWARD_SEARCH);
        boolean back = (b != null && b.booleanValue());
        if (oppositeDir) {
            back = !back;
        }
        return back;
    }
    
    private void addCaretSelectText(JTextComponent c, int start, int end, boolean back) {
        Caret eCaret = c.getCaret();
        ensureVisible(c, start, end);
        if (eCaret instanceof EditorCaret) {
            EditorCaret caret = (EditorCaret) eCaret;
            try {
                caret.addCaret(c.getDocument().createPosition(end), Position.Bias.Forward,
                    c.getDocument().createPosition(start), Position.Bias.Forward);
            } catch (BadLocationException ex) {
                Exceptions.printStackTrace(ex);
            }
        }
    }

    private void selectText(JTextComponent c, int start, int end, boolean back){
        Caret caret = c.getCaret();
        ensureVisible(c, start, end);
        if (back) {
            caret.setDot(end);
            caret.moveDot(start);
        } else { // forward direction
            caret.setDot(start);
            caret.moveDot(end);
        }
    }
    
    private void ensureVisible(JTextComponent c, int startOffset, int endOffset) {
        // TODO: read insets from settings
        ensureVisible(c, startOffset, endOffset, new Insets(10, 10, 10, 10));
    }
    
    /**
     * Ensure that the given region will be visible in the view
     * with the appropriate find insets.
     */
    private void ensureVisible(JTextComponent c, int startOffset, int endOffset, Insets extraInsets) {
        try {
            Rectangle startBounds = c.modelToView(startOffset);
            Rectangle endBounds = c.modelToView(endOffset);
            if (startBounds != null && endBounds != null) {
                startBounds.add(endBounds);
                if (extraInsets != null) {
                    Rectangle visibleBounds = c.getVisibleRect();
                    int extraTop = (extraInsets.top < 0)
                        ? -extraInsets.top * visibleBounds.height / 100 // percentage
                        : extraInsets.top * endBounds.height; // line count
                    startBounds.y -= extraTop;
                    startBounds.height += extraTop;
                    startBounds.height += (extraInsets.bottom < 0)
                        ? -extraInsets.bottom * visibleBounds.height / 100 // percentage
                        : extraInsets.bottom * endBounds.height; // line count
                    int extraLeft = (extraInsets.left < 0)
                        ? -extraInsets.left * visibleBounds.width / 100 // percentage
                        : extraInsets.left * endBounds.width; // char count
                    startBounds.x -= extraLeft;
                    startBounds.width += extraLeft;
                    startBounds.width += (extraInsets.right < 0)
                        ? -extraInsets.right * visibleBounds.width / 100 // percentage
                        : extraInsets.right * endBounds.width; // char count
                }
                c.scrollRectToVisible(startBounds);
            }
        } catch (BadLocationException e) {
            // do not scroll
        }
    }
    
    private int[] findMatches = null;
    private synchronized boolean findMatches(final String text, final Map<String, Object> props) {
        if(text == null) {
            return false;
        }
        try {
            final PlainDocument plainDocument = new PlainDocument();
            plainDocument.insertString(0, text, null);
            findMatches = null;
            try {
                executor.submit(new Runnable() {

                    @Override
                    public void run() {
                        try {
                            findMatches = DocumentFinder.find(plainDocument, 0, text.length(), props, false);
                        } catch (BadLocationException ble) {
                            LOG.log(Level.INFO, ble.getMessage(), ble);
                        }
                    }
                }).get(TIME_LIMIT, TimeUnit.SECONDS);
            } catch (InterruptedException | ExecutionException | TimeoutException ex) {
                org.netbeans.editor.Utilities.setStatusBoldText(getFocusedTextComponent(), NbBundle.getMessage(
                        EditorFindSupport.class, "slow-search"));
                LOG.log(Level.INFO, ex.getMessage(), ex);
            }
            return findMatches != null && findMatches[0] != -1;
        } catch (BadLocationException ex) {
            return false;
        }
    }
    
    FindReplaceResult findReplaceImpl(String replaceExp, 
            Map<String, Object> props, boolean oppositeDir, JTextComponent c) {
        incSearchReset();
        props = getValidFindProperties(props);
        boolean back = isBackSearch(props, oppositeDir);
        if (props.get(FIND_WHAT) == null || !(props.get(FIND_WHAT) instanceof String)) {
            return null;
        }
        String findWhat = (String) props.get(FIND_WHAT);
        if (c != null) {
            ComponentUtils.clearStatusText(c);
            Caret caret = c.getCaret();
            int dotPos = caret.getDot();
            if (findMatches(c.getSelectedText(), props)) {
                Object dp = props.get(FIND_BACKWARD_SEARCH);
                boolean direction = (dp != null) ? ((Boolean)dp).booleanValue() : false;

                if (dotPos == (oppositeDir ^ direction ? c.getSelectionEnd() : c.getSelectionStart())) {
                    dotPos += (oppositeDir ^ direction ? -1 : 1);
                }
                
                if (replaceExp != null) {
                    if (oppositeDir ^ direction) {
                        dotPos = c.getSelectionEnd();
                    } else {
                        dotPos = c.getSelectionStart();
                    }
                }
            }
            
            Boolean b = (Boolean)props.get(FIND_BLOCK_SEARCH);
            boolean blockSearch = (b != null && b.booleanValue());
            Position blockStartPos = (Position) props.get(FIND_BLOCK_SEARCH_START);
            int blockSearchStart = (blockStartPos != null) ? blockStartPos.getOffset() : -1;
            int blockSearchEnd = getBlockEndOffset();

            boolean backSearch = Boolean.TRUE.equals(props.get(FIND_BACKWARD_SEARCH));
            if (backSearch) {
                blockSearchEnd = dotPos;
                dotPos = 0;
            }
            try {
                FindReplaceResult result = findReplaceInBlock(replaceExp, c, dotPos, 
                        (blockSearch && blockSearchStart > -1) ? blockSearchStart : 0, 
                        (blockSearch && blockSearchEnd > 0) ? blockSearchEnd : backSearch ? blockSearchEnd : -1, 
                        props, oppositeDir);
                
                if (result != null && result.hasErrorMsg()) {
                    ComponentUtils.setStatusText(c, result.getErrorMsg());
                    c.getCaret().setDot(c.getCaret().getDot());
                    return null;
                }
                int[] blk = null; 
                if (result != null){
                    blk = result.getFoundPositions();
                }
                if (blk != null) {
                    if (Boolean.TRUE.equals(props.get(EditorFindSupport.ADD_MULTICARET))) {
                        addCaretSelectText(c, blk[0], blk[1], back);
                    } else {
                        selectText(c, blk[0], blk[1], back);
                    }
                    String msg = NbBundle.getMessage(EditorFindSupport.class, FOUND_LOCALE, findWhat, DocUtils.debugPosition(c.getDocument(), Integer.valueOf(blk[0])));
//                    String msg = exp + NbBundle.getMessage(EditorFindSupport.class, FOUND_LOCALE)
//                                 + ' ' + DocUtils.debugPosition(c.getDocument(), blk[0]);
                    if (blk[2] == 1) { // wrap was done
                        msg += "; "; // NOI18N
                        if (blockSearch && blockSearchEnd>0 && blockSearchStart >-1){
                            msg += back ? NbBundle.getMessage(EditorFindSupport.class, WRAP_BLOCK_END_LOCALE)
                                   : NbBundle.getMessage(EditorFindSupport.class, WRAP_BLOCK_START_LOCALE);
                        }else{
                            msg += back ? NbBundle.getMessage(EditorFindSupport.class, WRAP_END_LOCALE)
                                   : NbBundle.getMessage(EditorFindSupport.class, WRAP_START_LOCALE);
                        }
                        ComponentUtils.setStatusText(c, msg, IMPORTANCE_FIND_OR_REPLACE);
                        c.getToolkit().beep();
                    } else {
                        ComponentUtils.setStatusText(c, msg, IMPORTANCE_FIND_OR_REPLACE);
                    }
                    return result;
                } else { // not found
                    ComponentUtils.setStatusText(c, NbBundle.getMessage(
                                                    EditorFindSupport.class, NOT_FOUND_LOCALE, findWhat), IMPORTANCE_FIND_OR_REPLACE);
                    // issue 14189 - selection was not removed
                    c.getCaret().setDot(c.getCaret().getDot());
                    }
            } catch (BadLocationException e) {
                LOG.log(Level.WARNING, e.getMessage(), e);
            }
        }
        return null;
    }
    
    /** Find the text from the caret position.
    * @param localProps search properties
    * @param oppositeDir whether search in opposite direction
    */
    public boolean find(Map<String, Object> props, boolean oppositeDir) {
        FindReplaceResult result = findReplaceImpl(null, props, oppositeDir, getFocusedTextComponent());
        return (result != null);
    }

    private FindReplaceResult currentResult = null;
    private synchronized FindReplaceResult findReplaceInBlock(final String replaceExp, JTextComponent c, int startPos, int blockStartPos,
                             int blockEndPos, Map<String, Object> props, final boolean oppositeDir) throws BadLocationException {
        if (c != null) {
            final Map<String, Object> validProps = getValidFindProperties(props);
            final Document doc = c.getDocument();
            int pos = -1;
            boolean wrapDone = false;
            String replaced = null;

            boolean back = isBackSearch(validProps, oppositeDir);
            Boolean b = (Boolean)validProps.get(FIND_WRAP_SEARCH);
            boolean wrap = (b != null && b.booleanValue());
            int docLen = doc.getLength();
            if (blockEndPos == -1) {
                blockEndPos = docLen;
            }
            if (startPos == -1) {
                startPos = docLen;
            }

            int retFind[];
            while (true) {
                //pos = doc.find(sf, startPos, back ? blockStartPos : blockEndPos);
                final int off1 = startPos;
                final int off2 = oppositeDir ? blockStartPos : blockEndPos;
                currentResult = null;
                try {
                    executor.submit(new Runnable() {

                        @Override
                        public void run() {
                            try {
                                currentResult = DocumentFinder.findReplaceResult(replaceExp, doc, off1, off2,
                                        validProps, oppositeDir);
                            } catch (BadLocationException ble) {
                                LOG.log(Level.WARNING, ble.getMessage(), ble);
                            }

                        }
                    }).get(TIME_LIMIT, TimeUnit.SECONDS);
                } catch (InterruptedException | ExecutionException | TimeoutException ex) {
                    org.netbeans.editor.Utilities.setStatusBoldText(getFocusedTextComponent(), NbBundle.getMessage(
                            EditorFindSupport.class, "slow-search"));
                    LOG.log(Level.INFO, ex.getMessage(), ex);
                }

                if (currentResult == null) {     
                    return null;
                }
                
                if (currentResult.hasErrorMsg()) {
                    return currentResult;
                }
                retFind = currentResult.getFoundPositions();
                replaced = currentResult.getReplacedString();
                if (retFind == null){
                    break;
                }
                pos = retFind[0];
                
                if (pos != -1) {
                    break;
                }

                if (wrap) {
                    if (back) {
                        //Bug #20552 the wrap search check whole document
                        //instead of just the remaining not-searched part to be
                        //able to find expressions with the cursor in it

                        //blockStartPos = startPos;
                        startPos = blockEndPos;
                        blockEndPos = docLen;
                    } else {
                        //blockEndPos = startPos;
                        startPos = blockStartPos;
                    }
                    wrapDone = true;
                    wrap = false; // only one loop
                } else { // no wrap set
                    break;
                }

            }
            if (pos != -1) {
                int[] ret = new int[3];
                ret[0] = pos;
                ret[1] = retFind[1];
                ret[2] = wrapDone ? 1 : 0;
                
                return new FindReplaceResult(ret, replaced);
            }
        }
        return null;
    }
    
    /** Find the searched expression
    * @param startPos position from which to search. It must be inside the block.
    * @param blockStartPos starting position of the block. It must
    *   be valid position greater or equal than zero. It must be lower than
    *   or equal to blockEndPos (except blockEndPos=-1).
    * @param blockEndPos ending position of the block. It can be -1 for the end
    *   of document. It must be greater or equal than blockStartPos (except blockEndPos=-1).
    * @param localProps search properties
    * @param oppositeDir whether search in opposite direction
    * @param displayWrap whether display messages about the wrapping
    * @return either null when nothing was found or integer array with three members
    *    ret[0] - starting position of the found string
    *    ret[1] - ending position of the found string
    *    ret[2] - 1 or 0 when wrap was or wasn't performed in order to find the string 
    */
    public int[] findInBlock(JTextComponent c, int startPos, int blockStartPos,
                             int blockEndPos, Map<String, Object> props, boolean oppositeDir) throws BadLocationException {
        FindReplaceResult result = findReplaceInBlock(null, c, startPos, blockStartPos,
                             blockEndPos, props, oppositeDir);
        return result == null ? null : result.getFoundPositions();
    }

    public boolean replace(Map<String, Object> props, boolean oppositeDir)
    throws BadLocationException {
        incSearchReset();
        return replaceImpl(props, oppositeDir, getFocusedTextComponent());
    }

    boolean replaceImpl(Map<String, Object> props, boolean oppositeDir, JTextComponent c) throws BadLocationException {
        props = getValidFindProperties(props);
        boolean back = Boolean.TRUE.equals(props.get(FIND_BACKWARD_SEARCH));
        if (oppositeDir) {
            back = !back;
        }
        boolean blockSearch = Boolean.TRUE.equals(props.get(FIND_BLOCK_SEARCH));
        Position blockSearchStartPos = (Position) props.get(FIND_BLOCK_SEARCH_START);
        int blockSearchStartOffset = (blockSearchStartPos != null) ? blockSearchStartPos.getOffset() : -1;

        if (c != null) {
            String s = (String)props.get(FIND_REPLACE_WITH);
            Caret caret = c.getCaret();
            if (caret.isSelectionVisible() && caret.getDot() != caret.getMark()){
                Object dp = props.get(FIND_BACKWARD_SEARCH);
                boolean direction = (dp != null) ? ((Boolean)dp).booleanValue() : false;
                int dotPos = (oppositeDir ^ direction ? c.getSelectionEnd() : c.getSelectionStart());
                c.setCaretPosition(dotPos);
            }
            
            FindReplaceResult result = findReplaceImpl(s, props, oppositeDir, c);
            if (result!=null){
                s  = result.getReplacedString();
            } else {
                return false;
            }

            Document doc = c.getDocument();
            int startOffset = c.getSelectionStart();
            int len = c.getSelectionEnd() - startOffset;
            DocUtils.atomicLock(doc);
            try {
                if (len > 0) {
                    doc.remove(startOffset, len);
                }
                if (s != null && s.length() > 0) {
                    try {
                        NavigationHistory.getEdits().markWaypoint(c, startOffset, false, true);
                    } catch (BadLocationException e) {
                        LOG.log(Level.WARNING, "Can't add position to the history of edits.", e); //NOI18N
                    }
                    doc.insertString(startOffset, s, null);
                    if (startOffset == blockSearchStartOffset) { // Replaced at begining of block
                        blockSearchStartPos = doc.createPosition(startOffset);
                        props.put(EditorFindSupport.FIND_BLOCK_SEARCH_START, blockSearchStartPos);
                    }
                }
            } finally {
                DocUtils.atomicUnlock(doc);
                if (blockSearch){
                    setBlockSearchHighlight(blockSearchStartOffset, getBlockEndOffset());
                }
            }
            
            // adjust caret pos after replace operation
            int adjustedCaretPos = (back || s == null) ? startOffset : startOffset + s.length();
            caret.setDot(adjustedCaretPos);
            
        }
        
        return true;
    }

    public void replaceAll(Map<String, Object> props) {
        incSearchReset();
        replaceAllImpl(props, getFocusedTextComponent());
    }

    /**
     * This method is called from unit test. It is implementation of the above method.
     * @param props
     * @param c
     */
    void replaceAllImpl(Map<String, Object> props, JTextComponent c) {
        props = getValidFindProperties(props);
        Map<String,Object> localProps = new HashMap<>(props);
        String replaceWithOriginal = (String)localProps.get(FIND_REPLACE_WITH);

        Object findWhat = localProps.get(FIND_WHAT);
        if (findWhat == null) { // nothing to search for
            return;
        }
        if (findWhat.equals(replaceWithOriginal)) {
            return;
        }

        Document doc = c.getDocument();
        int maxCnt = doc.getLength();
        int replacedCnt = 0;
        int totalCnt = 0;

        boolean blockSearch = Boolean.TRUE.equals(localProps.get(FIND_BLOCK_SEARCH));
        boolean wrapSearch = Boolean.TRUE.equals(localProps.get(FIND_WRAP_SEARCH));
        boolean backSearch = Boolean.TRUE.equals(localProps.get(FIND_BACKWARD_SEARCH));

        if (wrapSearch){
            localProps.put(FIND_WRAP_SEARCH, Boolean.FALSE);
            localProps.put(FIND_BACKWARD_SEARCH, Boolean.FALSE);
            backSearch = false;
            firePropertyChange(null, null, null);
        }

        Position blockSearchStartPos = (Position) localProps.get(FIND_BLOCK_SEARCH_START);
        int blockSearchStartOffset = (blockSearchStartPos != null) ? blockSearchStartPos.getOffset() : -1;
        int blockSearchEndOffset = getBlockEndOffset();

        if (c != null) {
            DocUtils.atomicLock(doc);
            try {
                int startPosWholeSearch = 0;
                int endPosWholeSearch = -1;
                int caretPos = c.getCaret().getDot();

                if (!wrapSearch){
                    if (backSearch){
                        startPosWholeSearch = 0;
                        endPosWholeSearch = caretPos;
                    }else{
                        startPosWholeSearch = caretPos;
                        endPosWholeSearch = -1;
                    }
                }

                int actualPos = wrapSearch ? 0 : c.getCaret().getDot();

                int pos = (blockSearch && blockSearchStartOffset > -1) ?  blockSearchStartOffset : (backSearch? 0 : actualPos); // actual position
                while (true) {
                    FindReplaceResult result = findReplaceInBlock(replaceWithOriginal, c, pos,
                            (blockSearch && blockSearchStartOffset > -1) ? blockSearchStartOffset : startPosWholeSearch,
                            (blockSearch && blockSearchEndOffset > 0) ? blockSearchEndOffset : endPosWholeSearch,
                            localProps, false);
                    if (result == null){
                        break;
                    }
                    int[] blk = result.getFoundPositions();
                    String replaceWith = result.getReplacedString();
                    if (blk == null) {
                        break;
                    }
                    totalCnt++;
                    int len = blk[1] - blk[0];
                    boolean skip = false; // cannot remove (because of guarded block)?
                    try {
                        doc.remove(blk[0], len);
                    } catch (BadLocationException e) {
                        // replace in guarded block
                        if (ComponentUtils.isGuardedException(e)) {
                            skip = true;
                        } else {
                            throw e;
                        }
                    }
                    if (skip) {
                        pos = backSearch ? blk[0] : blk[0] + len;

                    } else { // can and will insert the new string
                        if (replaceWith != null && replaceWith.length() > 0) {
                            int offset = blk[0];
                            try {
                                NavigationHistory.getEdits().markWaypoint(c, offset, false, true);
                            } catch (BadLocationException e) {
                                LOG.log(Level.WARNING, "Can't add position to the history of edits.", e); //NOI18N
                            }
                            doc.insertString(offset, replaceWith, null);
                            if (offset == blockSearchStartOffset) { // Replaced at begining of block
                                blockSearchStartPos = doc.createPosition(offset);
                                // Update position in original properties
                                props.put(EditorFindSupport.FIND_BLOCK_SEARCH_START, blockSearchStartPos);
                            }
                            blockSearchEndOffset = getBlockEndOffset();
                        }
                        pos = backSearch ? blk[0] : blk[0] + ((replaceWith != null) ? replaceWith.length() : 0);
                        if (!wrapSearch && backSearch) {
                            endPosWholeSearch = endPosWholeSearch < blk[0] ? endPosWholeSearch : blk[0];
                            blockSearchEndOffset = blockSearchEndOffset < blk[0] ? blockSearchEndOffset : blk[0];
                            pos = (blockSearch && blockSearchStartOffset > -1) ?  blockSearchStartOffset : 0;
                        }
                        replacedCnt++;
                    }
                    // The following is lame attempt to break the loop: if
                    // someone knows a better way please remove this but check
                    // that all tests in EditorFindSupportTest pass!
                    if (replacedCnt > maxCnt) {
                        break;
                    }
                }

                // Display message about replacement
                if (totalCnt == 0){
                    String exp = "'" + findWhat + "' "; //NOI18N
                    ComponentUtils.setStatusText(c, exp + NbBundle.getMessage(
                                EditorFindSupport.class, NOT_FOUND_LOCALE), IMPORTANCE_FIND_OR_REPLACE);
                }else{
                    MessageFormat fmt = new MessageFormat(
                                            NbBundle.getMessage(EditorFindSupport.class, ITEMS_REPLACED_LOCALE));
                    String msg = fmt.format(new Object[] { Integer.valueOf(replacedCnt), Integer.valueOf(totalCnt) });
                    ComponentUtils.setStatusText(c, msg, IMPORTANCE_FIND_OR_REPLACE);
                }

            } catch (BadLocationException e) {
                LOG.log(Level.WARNING, e.getMessage(), e);
            } finally {
                DocUtils.atomicUnlock(doc);
                if (blockSearch){
                    setBlockSearchHighlight(blockSearchStartOffset, getBlockEndOffset());
                }
            }
        }
    }

    public void hookLayer(BlockHighlighting layer, JTextComponent component) {
        synchronized (comp2layer) {
            Map<String, WeakReference<BlockHighlighting>> type2layer = comp2layer.get(component);

            if (type2layer == null) {
                type2layer = new HashMap<>();
                comp2layer.put(component, type2layer);
            }

            type2layer.put(layer.getLayerTypeId(), new WeakReference<>(layer));
        }
    }
    
    public void unhookLayer(BlockHighlighting layer, JTextComponent component) {
        synchronized (comp2layer) {
            Map<String, WeakReference<BlockHighlighting>> type2layer = comp2layer.get(component);

            if (type2layer != null) {
                type2layer.remove(layer.getLayerTypeId());
                if (type2layer.isEmpty()) {
                    comp2layer.remove(component);
                }
            }
        }
    }
    
    public BlockHighlighting findLayer(JTextComponent component, String layerId) {
        synchronized (comp2layer) {
            Map<String, WeakReference<BlockHighlighting>> type2layer = comp2layer.get(component);
            BlockHighlighting layer = null;

            if (type2layer != null) {
                WeakReference<BlockHighlighting> ref = type2layer.get(layerId);
                if (ref != null) {
                    layer = ref.get();
                }
            }

            return layer;
        }
    }

    /** Add weak listener to listen to change of any property. The caller must
    * hold the listener object in some instance variable to prevent it
    * from being garbage collected.
    */
    public void addPropertyChangeListener(PropertyChangeListener l) {
        changeSupport.addPropertyChangeListener(l);
    }

    public synchronized void addPropertyChangeListener(String findPropertyName,
            PropertyChangeListener l) {
        changeSupport.addPropertyChangeListener(findPropertyName, l);
    }

    /** Remove listener for changes in properties */
    public void removePropertyChangeListener(PropertyChangeListener l) {
        changeSupport.removePropertyChangeListener(l);
    }

    /**
     * <p><b>IMPORTANT:</b> This method is public only for keeping backwards
     * compatibility of the {@link org.netbeans.editor.FindSupport} class.
     */
    public void firePropertyChange(String settingName, Object oldValue, Object newValue) {
        changeSupport.firePropertyChange(settingName, oldValue, newValue);
    }

    public void setHistory(List<SPW> spwList){
        this.historyList = new ArrayList<>(spwList);
        if (!spwList.isEmpty()) {
            setLastSelected(spwList.get(0));
//        firePropertyChange(FIND_HISTORY_CHANGED_PROP,null,null);
        }
    }
    
    public void setReplaceHistory(List<RP> rpList){
        this.replaceList = new ArrayList<>(rpList);
    }
    
    public List<SPW> getHistory(){
        if (historyList.isEmpty()) {
            firePropertyChange(FIND_HISTORY_CHANGED_PROP,null,null);
        }
        return historyList;
    }
    
    public List<RP> getReplaceHistory(){
        if (replaceList.isEmpty()) {
            firePropertyChange(REPLACE_HISTORY_CHANGED_PROP,null,null);
        }
        return replaceList;
    }
    
    public void setLastSelected(SPW spw){
        this.lastSelected = spw;
        Map<String, Object> props = getFindProperties();
        if (spw == null) {
            return;
        }
        props.put(FIND_WHAT, spw.getSearchExpression());
        props.put(FIND_MATCH_CASE, Boolean.valueOf(spw.isMatchCase()));
        props.put(FIND_REG_EXP, Boolean.valueOf(spw.isRegExp()));
        props.put(FIND_WHOLE_WORDS, Boolean.valueOf(spw.isWholeWords()));
    }
    
    public SPW getLastSelected(){
        return lastSelected;
    }
    
    public void addToHistory(SPW spw){
        if (spw == null) {
            return;
        }
        firePropertyChange(FIND_HISTORY_PROP, null, spw);
    }
    
    public void addToReplaceHistory(RP rp) {
        if (rp == null) {
            return;
        }
        firePropertyChange(REPLACE_HISTORY_PROP, null, rp);
    }

    private String calculateCacheKey(Document doc, int startOffset, int endOffset, Map<String, Object> props) {
        StringBuilder newCacheKey = new StringBuilder();
        newCacheKey.append("#").append(doc.getLength());
        newCacheKey.append("#").append(startOffset);
        newCacheKey.append("#").append(endOffset);
        newCacheKey.append("#").append(props.get(FIND_WHAT));
        newCacheKey.append("#").append(props.get(FIND_HIGHLIGHT_SEARCH));
        newCacheKey.append("#").append(props.get(FIND_INC_SEARCH));
        newCacheKey.append("#").append(props.get(FIND_BACKWARD_SEARCH));
        newCacheKey.append("#").append(props.get(FIND_WRAP_SEARCH));
        newCacheKey.append("#").append(props.get(FIND_MATCH_CASE));
        newCacheKey.append("#").append(props.get(FIND_SMART_CASE));
        newCacheKey.append("#").append(props.get(FIND_WHOLE_WORDS));
        newCacheKey.append("#").append(props.get(FIND_REG_EXP));
        newCacheKey.append("#").append(props.get(FIND_BLOCK_SEARCH));
        newCacheKey.append("#").append(props.get(FIND_BLOCK_SEARCH_START));
        newCacheKey.append("#").append(props.get(FIND_BLOCK_SEARCH_END));
        return newCacheKey.toString();
    }
    
    public final static class SPW{
        private final String searchExpression;
        private final boolean wholeWords;
        private final boolean matchCase;
        private final boolean regExp;
        
        public SPW(String searchExpression, boolean wholeWords,
            boolean matchCase, boolean regExp){
            this.searchExpression = searchExpression;
            this.wholeWords = wholeWords;
            this.matchCase = matchCase;
            this.regExp = regExp;
        }
        
        /** @return searchExpression */
        public String getSearchExpression(){
            return searchExpression;
        }

        /** @return true if the wholeWords parameter was used during search performing */
        public boolean isWholeWords(){
            return wholeWords;
        }

        /** @return true if the matchCase parameter was used during search performing */
        public boolean isMatchCase(){
            return matchCase;
        }
        
        /** @return true if the regExp parameter was used during search performing */
        public boolean isRegExp(){
            return regExp;
        }

        public @Override boolean equals(Object obj){
            if (!(obj instanceof SPW)){
                return false;
            }
            SPW sp = (SPW)obj;
            return (this.searchExpression.equals(sp.getSearchExpression()) &&
                    this.wholeWords == sp.isWholeWords() &&
                    this.matchCase == sp.isMatchCase() &&
                    this.regExp == sp.isRegExp());
        }

        public @Override int hashCode() {
            int result = 17;
            result = 37*result + (this.wholeWords ? 1:0);
            result = 37*result + (this.matchCase ? 1:0);
            result = 37*result + (this.regExp ? 1:0);
            result = 37*result + this.searchExpression.hashCode();
            return result;
        }
        
        public @Override String toString(){
            StringBuilder sb = new StringBuilder("[SearchPatternWrapper:]\nsearchExpression:"+searchExpression);//NOI18N
            sb.append('\n');
            sb.append("wholeWords:");//NOI18N
            sb.append(wholeWords);
            sb.append('\n');
            sb.append("matchCase:");//NOI18N
            sb.append(matchCase);
            sb.append('\n');
            sb.append("regExp:");//NOI18N
            sb.append(regExp);
            return  sb.toString();
        }
    } // End of SPW class

    public final static class RP {

        private final String replaceExpression;
        private final boolean preserveCase;

        public RP(String replaceExpression, boolean preserveCase) {
            this.replaceExpression = replaceExpression;
            this.preserveCase = preserveCase;
        }

        public String getReplaceExpression() {
            return replaceExpression;
        }

        public boolean isPreserveCase() {
            return preserveCase;
        }

        @Override
        public boolean equals(Object obj) {
            if (!(obj instanceof RP)) {
                return false;
            }
            RP sp = (RP) obj;
            return (this.replaceExpression.equals(sp.getReplaceExpression())
                    && this.preserveCase == sp.isPreserveCase());
        }

        @Override
        public int hashCode() {
            int result = 17;
            result = 37 * result + (this.preserveCase ? 1 : 0);
            result = 37 * result + this.replaceExpression.hashCode();
            return result;
        }
    }
}