/*
 * 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.i18n.form;


import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.util.Collection;
import java.util.Comparator;
import java.util.Iterator;
import java.util.NoSuchElementException;
import java.util.ResourceBundle;
import java.util.TreeSet;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import javax.swing.text.AbstractDocument;
import javax.swing.text.BadLocationException;
import javax.swing.text.Element;
import javax.swing.text.Position;
import javax.swing.text.StyledDocument;
import org.netbeans.api.editor.guards.GuardedSection;
import org.netbeans.api.editor.guards.GuardedSectionManager;

import org.netbeans.modules.form.FormDataObject;
import org.netbeans.modules.form.FormModel;
import org.netbeans.modules.form.FormProperty;
import org.netbeans.modules.form.RADComponent;
import org.netbeans.modules.form.RADConnectionPropertyEditor.RADConnectionDesignValue;
import org.netbeans.modules.i18n.HardCodedString;
import org.netbeans.modules.i18n.I18nString;
import org.netbeans.modules.i18n.I18nSupport;
import org.netbeans.modules.i18n.InfoPanel;
import org.netbeans.modules.i18n.java.JavaI18nFinder;
import org.netbeans.modules.i18n.java.JavaI18nString;
import org.netbeans.modules.i18n.java.JavaI18nSupport;

import org.netbeans.modules.nbform.FormEditorSupport;
import org.openide.loaders.DataObject;
import org.openide.nodes.Node;
import org.openide.NotifyDescriptor;
import org.openide.DialogDisplayer;
import org.openide.ErrorManager;
import org.openide.cookies.EditorCookie;
import org.openide.nodes.Node.PropertySet;
import org.openide.util.NbBundle;


/** 
 * Support for internationalizing strings in java sources with forms.
 *
 * @author Peter Zavadsky
 */
public class FormI18nSupport extends JavaI18nSupport {

    //see RADComponent and  RADVisualComponent
    private static final String RAD_PROPERTIES = "properties"; // NOI18N
    private static final String RAD_PROPERTIES2 = "properties2"; // NOI18N
    private static final String RAD_ACCESSIBILITY = "accessibility"; // NOI18N
    private static final String RAD_LAYOUT = "layout"; // NOI18N

// PENDING - Has not been used because of implementation of method
//           isGuardedPosition(int)  (now replaced with isInGuardedSection(...))
//           The replacement for the isGuardedPosition method was made
//           during bugfixing phase so the documentListener remained disabled
//           to keep the amount of changes small (and thus minimize the risk
//           of introducing new bugs.
    
//    /** Listener which listens on changes of form document. */
//    DocumentListener documentListener;
    
    /** Constructor. */
    private FormI18nSupport(DataObject sourceDataObject) {
        super(sourceDataObject);
    }

    
    /** Creates <code>I18nFinder</code>. */
    @Override
    protected I18nFinder createFinder() {
        return new FormI18nFinder(sourceDataObject, document);
    }

    /** Creates <code>I18nReplacer</code>. */
    @Override
    protected I18nReplacer createReplacer() {        
        return new FormI18nReplacer(/*sourceDataObject, document,*/ (FormI18nFinder)getFinder());
        
// PENDING - Has not been used because of implementation of method
//           isGuardedPosition(int)  (now replaced with isInGuardedSection(...))
//           See the first PENDING note.
        
//        documentListener = new DocumentListener() {
//            public void changedUpdate(DocumentEvent e) {
//            }

//            public void insertUpdate(DocumentEvent e) {
//                updateFormProperties();
//            }

//            public void removeUpdate(DocumentEvent e) {
//            }
//        };
        
//        document.addDocumentListener(documentListener);
    }
    
    /** Gets info panel about found hard string. */
    @Override
    public JPanel getInfo(HardCodedString hcString) {
        return new FormInfoPanel(hcString, document);
    }
    
    /** Helper method. */
    static String toAscii(String str) {
        if (str == null) {
            return null;
        }
        // Note: All this code is copied from org.netbeans.core.editors.StringEditor.
        StringBuffer buf = new StringBuffer(str.length() * 6); // x -> \u1234
        char[] chars = str.toCharArray();
        for (int i = 0; i < chars.length; i++) {
            char c = chars[i];
            switch (c) {
            case '\b': buf.append("\\b"); break; // NOI18N
            case '\t': buf.append("\\t"); break; // NOI18N
            case '\n': buf.append("\\n"); break; // NOI18N
            case '\f': buf.append("\\f"); break; // NOI18N
            case '\r': buf.append("\\r"); break; // NOI18N
            case '\"': buf.append("\\\""); break; // NOI18N
                //        case '\'': buf.append("\\'"); break; // NOI18N
            case '\\': buf.append("\\\\"); break; // NOI18N
            default:
                if (c >= 0x0020 && c <= 0x007f) {
                    buf.append(c);
                } else {
                    buf.append("\\u"); // NOI18N
                    String hex = Integer.toHexString(c);
                    for (int j = 0; j < 4 - hex.length(); j++) {
                        buf.append('0');
                    }
                    buf.append(hex);
                }
            }
        }
        return buf.toString();
    }

    /** Inner class for holding info about form proeprties which can include hardcoded string.
     * see formProperties variable in enclosing class. */
    private static class ValidFormProperty {
        /** Holds property of form. */
        private Node.Property property;
        
        /** Holds rad component name. */
        private String radCompName;
        
        /** How many occurences of found string should be skipped in this property.
         * 0 means find the first occurence.
         * All this just means that the property (mostly 'code generation-> pre-init, post-init etc.' properties)
         * could contain more than one occurence of found string
         * and in that case is very important to match and replace the same found in document. */
        private int skip;
   

        /** Constructor. */
        public ValidFormProperty(String radCompName, Node.Property property) {
            this.radCompName = radCompName;
            this.property    = property;
            this.skip        = 0;
        }

        /** Constructor. */
        public ValidFormProperty(ValidFormProperty validProperty) {
            radCompName = validProperty.getRADComponentName();
            property = validProperty.getProperty();
            skip = validProperty.getSkip();
        }
        
        
        /** Getter for <code>radCompName</code> property. */
        public String getRADComponentName() {
            return radCompName;
        }

        /** Getter for property.
         * @return property can contain hard-coded string */
        public Node.Property getProperty() {
            return property;
        }
        
        /** Getter for skip.
         * @return amount of occurences of hard-coded string to skip */
        public int getSkip() {
            return skip;
        }
        
        /** Increment the amount of occurences to skip. */
        public void incrementSkip() {
            skip++;
        }

        /** Decrement skip amount of occurences to skip. */
        public void decrementSkip() {
            if (skip > 0) {
                skip--;
            }
        }
        
    } // end of ValidFormProperty inner class

    /**
     * Iterator over form properties of a component.
     * Form properties are defined as properties from property sets named
     * &quot;properties&quot;, &quot;properties2&quot;,
     * &quot;accessibility&quot; and &quot;layout&quot;.
     * 
     * @author  Marian Petras
     */
    static final class FormPropertiesIterator implements Iterator<Node.Property> {

        private final RADComponent comp;

        private boolean ready = false;
        private Node.PropertySet[] propSets;
        private int propSetIndex = -1;
        private Node.Property[] properties;
        private int propIndex = -1;
        private boolean first = true;
        private boolean exhausted = false;

        FormPropertiesIterator(RADComponent comp) {
            this.comp = comp;
        }
        
        public boolean hasNext() {
            if (!ready) {
                if (first || (++propIndex == properties.length)) {
                    if (first) {
                        propSets = comp.getProperties();
                        first = false;
                    }
                    while (++propSetIndex != propSets.length) {
                        Node.PropertySet propSet;
                        if (isFormPropSet(propSet = propSets[propSetIndex])
                                && ((properties = propSet.getProperties()).length != 0)) {
                            propIndex = 0;
                            break;
                        }
                    }
                    if (propSetIndex == propSets.length) {
                        exhausted = true;
                    }
                }
                ready = true;
            }
            return !exhausted;
        }

        private static boolean isFormPropSet(PropertySet propertySet) {
            String propSetName = propertySet.getName();
            return RAD_PROPERTIES.equals(propSetName)
                   || RAD_PROPERTIES2.equals(propSetName)
                   || RAD_LAYOUT.equals(propSetName)
                   || RAD_ACCESSIBILITY.equals(propSetName);
        }

        public Node.Property next() {
            if (!hasNext()) {
                throw new NoSuchElementException();
            }

            ready = false;
            return properties[propIndex];
        }

        public void remove() {
            throw new UnsupportedOperationException();
        }

    }
    
    /** Helper inner class for formProperties variable in enclosing class.
     * Provides sorting of ValidPropertyComparator classes with intenetion to get the order of
     * properties to match order like they are generated to initComponents form guarded block.
     * It has four stages of comparing two properies.
     * 1) the property which belongs to creation block (preCreationCode, customCreationCode, postCreationCode)
     *   is less (will be generated sooner) then property which is from init block(other names).
     * 2) than the property which component was added to form sooner is less then property which component was
     *   added later. (Top-level component is the least one.)
     * 3) than a) creation block: preCreationCode < (is less) customCreationCode < postCreationCode
     *         b) init block: preInitCode < set-method-properties < postInitCode
     * 4) than (for init block only) in case of set-method-properties. The property is less which has less index in
     *   array returned by method getAllProperties on component.
     * */
    private static class ValidFormPropertyComparator implements Comparator<ValidFormProperty> {
        
        private static final String CREATION_CODE_PRE    = "creationCodePre"; // NOI18N
        private static final String CREATION_CODE_CUSTOM = "creationCodeCustom"; // NOI18N
        private static final String CREATION_CODE_POST   = "creationCodePost"; // NOI18N
        
        private static final String INIT_CODE_PRE  = "initCodePre"; // NOI18N
        private static final String INIT_CODE_POST = "initCodePost"; // NOI18N

        /** <code>FormModel</code> on which <code>FormDataObject</code> the i18n session runs.*/
        private final FormModel formModel;
        
        
        /** Constructor. */
        public ValidFormPropertyComparator(FormDataObject formDataObject) {
            FormEditorSupport fes = (FormEditorSupport)formDataObject.getFormEditorSupport();
            formModel = fes.getFormModel();
        }
        
        
        /** Compares two <code>ValidFormPropertiesObjects</code>. */
        public int compare(ValidFormProperty p1, ValidFormProperty p2) {
            Node.Property prop1 = p1.getProperty();
            Node.Property prop2 = p2.getProperty();

            // 1st stage
            String propName1 = prop1.getName();
            String propName2 = prop2.getName();
            
            boolean isInCreation1 = false;
            boolean isInCreation2 = false;
            
            if (propName1.equals(CREATION_CODE_PRE)
                    || propName1.equals(CREATION_CODE_CUSTOM)
                    || propName1.equals(CREATION_CODE_POST)) {
                isInCreation1 = true;
            }
            
            if (propName2.equals(CREATION_CODE_PRE)
                    || propName2.equals(CREATION_CODE_CUSTOM)
                    || propName2.equals(CREATION_CODE_POST)) {
                isInCreation2 = true;
            }
            
            if(isInCreation1 != isInCreation2) {
                return isInCreation1 ? -1 : 1; // end of 1st stage
            } // end of 1st stage
                
            // 2nd stage
            RADComponent comp1 = formModel.findRADComponent(p1.getRADComponentName());
            RADComponent comp2 = formModel.findRADComponent(p2.getRADComponentName());
            
            int index1 = -1;
            int index2 = -1;
            
            if (!comp1.equals(comp2)) {
                for (RADComponent comp : formModel.getOrderedComponentList()) {
                    if (comp == comp1) {
                        return -1;
                    }
                    if (comp == comp2) {
                        return 1;
                    }
                }
                assert false;
                return 0;
            } // end of 2nd stage
            
            // 3rd stage
            if (isInCreation1) {
                // 3a) stage
                index1 = -1;
                index2 = -1;
                
                if (propName1.equals(CREATION_CODE_PRE)) {
                    index1 = 0;
                } else if (propName1.equals(CREATION_CODE_CUSTOM)) {
                    index1 = 1;
                } else if (propName1.equals(CREATION_CODE_POST)) {
                    index1 = 2;
                }
                
                if (propName2.equals(CREATION_CODE_PRE)) {
                    index2 = 0;
                } else if (propName2.equals(CREATION_CODE_CUSTOM)) {
                    index2 = 1;
                } else if (propName2.equals(CREATION_CODE_POST)) {
                    index2 = 2;
                }
                
                return index1 - index2; // end of 3a) stage
            } else {
                // 3b) stage
                index1 = -1;
                index2 = -1;
                
                if (propName1.equals(INIT_CODE_PRE)) {
                    index1 = 0;
                } else if (propName1.equals(INIT_CODE_POST)) {
                    index1 = 2;
                } else {
                    index1 = 1; // is one of set-method property
                }
                
                if (propName2.equals(INIT_CODE_PRE)) {
                    index2 = 0;
                } else if (propName2.equals(INIT_CODE_POST)) {
                    index2 = 2;
                } else {
                    index2 = 1; // is one of set-method property
                }
                
                if ((index1 != 1) || (index2 != 1)) {
                    return index1 - index2; // end of 3b) stage
                }
            } // end of 3rd stage
            
            // 4th stage
            index1 = -1;
            index2 = -1;
            Iterator<Node.Property> it = new FormPropertiesIterator(comp1);
            for (int propIndex = 0; it.hasNext(); propIndex++) {
                Node.Property property = it.next();
                if (prop1.equals(property)) {
                    index1 = propIndex;
                }
                if (prop2.equals(property)) {
                    index2 = propIndex;
                }
                if ((index1 != -1) && (index2 != -1)) {
                    break;
                }
            }
            return index1 - index2; // end of 4th stage

        } // end of compare method
   
    } // End of ValidFormPropertyCompoarator inner class.

    
    /** Nested class. Finder in java source with form. */
    private static class FormI18nFinder extends JavaI18nFinder {
        /** */
        private DataObject sourceDataObject;
        
        /** Holds name of component which property has hardcoded string. */
        private String componentName = ""; // NOI18N

        /** Holds name of property with found hardcoded string */
        private String propertyName = ""; // NOI18N

        /** Collection for holding properties of form and their components, if search is in form performed. */
        private TreeSet<ValidFormProperty> formProperties;

        /** Found valid form property from last search. */
        private ValidFormProperty lastFoundProp;

        
        /** Constructor. */
        public FormI18nFinder(DataObject sourceDataObject, StyledDocument document) {
            super(document);
            this.sourceDataObject = sourceDataObject;
            
            init();
        }
        
        
        /** Initializes finder. */
        private void init() {
            clearFormInfoValues();
            
            lastFoundProp = null;
            
            createFormProperties();
        }

        /** Resets finder. */
        @Override
        protected void reset() {
            super.reset();
            
            init();
        }
        
        /** Decrements skip value of last found property. Called from replacer. */
        void decrementLastFoundSkip() {
            if (lastFoundProp != null) {
                lastFoundProp.decrementSkip();
            }
        }

        /** Cretaes collection of properties of form which are value type of String.class
         * and could have null value (it saves time to determine the cases the value was changed).
         * Collection is referenced to formProperties variable.
         * @return True if sorted collection was created. */
        private synchronized boolean createFormProperties() {
            // creates new collection
            formProperties = new TreeSet<ValidFormProperty>(
                    new ValidFormPropertyComparator((FormDataObject) sourceDataObject));
            updateFormProperties();

            return true;
        }

        /** Updates collection in formProperties variable. */
        private synchronized void updateFormProperties() {
            if (formProperties == null) {
                return;
            }

            // All components in current FormDataObject.
            FormDataObject formDataObject = (FormDataObject) sourceDataObject;
            FormEditorSupport fes = (FormEditorSupport)formDataObject.getFormEditorSupport();
            Collection<RADComponent> c = fes.getFormModel().getAllComponents();

            // search thru all RADComponents in the form
            for (RADComponent radComponent : c) {

                Iterator<Node.Property> it = new FormPropertiesIterator(radComponent);
                while (it.hasNext()) {
                    Node.Property property = it.next();

                    // skip hidden and unchanged properties
                    if (property.isHidden()
                            || !(property instanceof FormProperty)
                            || !((FormProperty)property).isChanged()) {
                        continue;
                    }

                    // get value
                    Object value;
                    try {
                        value = property.getValue();
                    } catch(IllegalAccessException iae) {
                        continue; // next property
                    } catch(InvocationTargetException ite) {
                        continue; // next property
                    }

                    // Property have to be a non-null value and have "value type" of String (don't confuse with the type of object referred by value variable!!)
                    // or value be RADconnectiondesignValue and be type of TYPE_VALUE or TYPE_CODE
                    if(value != null && (property.getValueType().equals(String.class)
                        || (value instanceof RADConnectionDesignValue
                            &&  ( ((RADConnectionDesignValue)value).getType() == RADConnectionDesignValue.TYPE_VALUE
                                || ((RADConnectionDesignValue)value).getType() == RADConnectionDesignValue.TYPE_CODE) 
                            ) )
                        ) {
                        // Actually add the property to the list.
                        // Note: add only ValidFormProperty instances
                        formProperties.add(new ValidFormProperty(radComponent.getName(), property));
                    }
                }
            }
        }
        
        @Override
        protected HardCodedString findNextString() {
            //boolean found;
            HardCodedString hcString;

            clearFormInfoValues();
            
            boolean guarded = false;

            do {
                hcString = super.findNextString();

                // If i18n search we are not interesting in form values.
                if (i18nSearch) {
                    return hcString;
                }
                
                if (hcString != null) {
                    guarded = isInGuardedSection(hcString.getStartPosition(),
                                                 hcString.getEndPosition());
                } else {
                    // No more hardcoded strings in source.
                    break;
                }

                if (guarded) {
                    hcString = findInForm(hcString);

                    // Skip found hardcoded string if it is in a guarded block and not found appropriate form component property.
                }

            // Skip found hardcoded string if it is in a guarded block and not found appropriate form component property.
            } while(guarded && (hcString == null));

            // PENDING
            // See first PENDING.
            //        if(!found)
            //            document.removeDocumentListener(documentListener);

            return hcString;
        }

        /** Helper method. */
        private void clearFormInfoValues() {
            componentName = ""; // NOI18N
            propertyName  = ""; // NOI18N
        }
        
        
        /** Analyzes the text in a guraded block, tries to find the name
         *  of the component and of the property which value matches
         *  with just found hardcoded string.
         */
        private synchronized HardCodedString findInForm(HardCodedString hcString) {
            boolean found = false;

            String hardString = hcString.getText();
            
            // Valid form property.
            ValidFormProperty validProp = null;

            // Node property.
            Node.Property nodeProperty = null;

            Iterator<ValidFormProperty> it;

            if (lastFoundProp != null) {
                validProp = lastFoundProp;
                it = formProperties.tailSet(lastFoundProp).iterator();
                /*
                 * In the first cycle of the following do-while loop, value
                 * of 'lastFoundProp' will be used, not the one from
                 * the iterator. So make sure the property following
                 * 'lastFoundProp' is used in the next cycle:
                 */
                it.next();
            } else {
                it = formProperties.iterator();
            }

            do {
                if (validProp == null && it.hasNext()) {
                    validProp = it.next();
                }
                if (validProp == null) {
                    break;
                }
                Node.Property property = validProp.getProperty();
                String radCompName = validProp.getRADComponentName();
                
                // get value
                Object value;
                try {
                    value = property.getValue();
                } catch(IllegalAccessException iae) {
                    ErrorManager.getDefault().notify(ErrorManager.INFORMATIONAL, iae);
                    validProp = null;
                    continue; // next property
                } catch(InvocationTargetException ite) {
                    ErrorManager.getDefault().notify(ErrorManager.INFORMATIONAL, ite);
                    validProp = null;
                    continue; // next property
                }

                // Property have to be a non-null value and have "value type" of String (don't confuse with the type of object referred by value variable!!)
                // or value be RADconnectiondesignValue and be type of TYPE_VALUE or TYPE_CODE
                if(value != null && (property.getValueType().equals(String.class)
                    || (value instanceof RADConnectionDesignValue
                        &&  ( ((RADConnectionDesignValue)value).getType() == RADConnectionDesignValue.TYPE_VALUE
                            || ((RADConnectionDesignValue)value).getType() == RADConnectionDesignValue.TYPE_CODE) 
                        ) )
                    ) {
                        
                    String string;
                    
                    if(property instanceof FormProperty) {
                        // RADProperty, the value could be constructed from one of PropertyEditors
                        if(value instanceof FormI18nString) {
                            // resource bundle value, do not replace, is internationalized already !!
                            validProp = null;
                            continue; // next property
                        } else if (value instanceof RADConnectionDesignValue) {
                            // is Form connection value
                            string = ""; // NOI18N
                            RADConnectionDesignValue connectionValue = (RADConnectionDesignValue)value;
                            if(connectionValue.getType() == RADConnectionDesignValue.TYPE_VALUE) {
                                // is type of VALUE
                                string = connectionValue.getValue();

                                if (indexOfNonI18nString(string, hardString, validProp.getSkip()) != -1) {
                                    found = true;
                                }
                            } else if (connectionValue.getType() == RADConnectionDesignValue.TYPE_CODE) {
                                // is type of TYPE_CODE
                                string = connectionValue.getCode();

                                if (indexOfNonI18nString(string, hardString, validProp.getSkip()) != -1) {
                                    found = true;
                                }
                            }
                        } else if(value instanceof String) { // #179872
                            // Has to be plain String, there is other Property Editor for String RAD Property.
                            // It's converted via toAscii method cause for this value is used org.netbeans.core.editors.StringEditor 
                            // which does the same thing.
                            string = toAscii((String)value);
                            
                            if ((validProp.getSkip() == 0) && string.equals(toAscii(hardString))) {
                                found = true;
                            }
                        } else {
                            // TODO: now, do nothing, but see #179872 :
                            // org.netbeans.modules.swingapp.ResourceValueImpl
                        }
                    } else {
                        // Node.Property, the value should be plain String.
                        string = (String)value;
                        
                        if (indexOfNonI18nString(string, hardString, validProp.getSkip()) != -1) {
                            // non-internationalized hardString found.
                            found = true;
                        }
                    }
                }
                if (found) {
                    nodeProperty = property;
                    componentName = radCompName;
                    propertyName = property.getName();

                    break;
                } else {
                    validProp = null;
                }
            } while(it.hasNext());

            if(found) {
                lastFoundProp = new ValidFormProperty(validProp);
                lastFoundProp.incrementSkip();

                return new FormHardCodedString(
                    hcString.getText(),
                    hcString.getStartPosition(),
                    hcString.getEndPosition(),
                    validProp,
                    nodeProperty
                );
            } else {
                return null;
            }
        }
        
        
        /** Getter for <code>lastPosition</code> property. */
        public Position getLastPosition() {
            return lastPosition;
        }
        
        /** Setter for <code>lastPosition</code> property. */
        public void setLastPosition(Position lastPosition) {
            super.lastPosition = lastPosition;
        }
        
        /** Helper method. */
        public int indexOfNonI18nString(String source, String hardString, int skip) {
            // Index of found string in code described by source string.
            int index = 0;

            // Start index for each iteration of loop.
            int startIndex=0;

            while (true) {
                // Find out if there could be some string yet.
                int startString = source.indexOf('\"', startIndex);

                if (startString == -1) {
                    break;
                }

                // Get the string.
                int endString = source.indexOf('\"', startString+1);

                int endLineIndex = source.indexOf('\n', startString+1);

                // Check for validity of that string. (It has to be in one row).
                if(endLineIndex == -1 || endString < endLineIndex) {
                    // Get valid string.
                    String foundString = source.substring(startString, endString+1);

                    // Construct part line which will be compared in with regular expression.
                    String partLine;
                    
                    // Adjust start index so the part line starts at the same line like found string.
                    int startLine = source.indexOf('\n', startIndex + 1);
                    if (startLine != -1 && startLine < startString) {
                        startIndex = startLine + 1;
                    }

                    if (endLineIndex == -1) {
                        partLine = source.substring(startIndex);
                    } else {
                        int quote = source.indexOf('\"', endString+1);

                        // If there is another string on that line cut part line before that.
                        if (quote != -1 && quote < endLineIndex) {
                            // The second part is little trick to cheat out regular expression in minor case the next string is same and i18n-ized already.
                            partLine = source.substring(startIndex, quote) + source.substring(quote, endLineIndex).replace('\"', '_');
                        } else {
                            partLine = source.substring(startIndex, endLineIndex);
                        }
                    }

                    // Compare with regular expression.
                    if(isSearchedString(partLine, foundString)) {
                        // Is non-i18n-ized string and has the has the order number in source string code we search for.
                        if (index == skip) {
                            if (foundString.equals("\""+hardString+"\"")) {
                                // It is our hard string.
                                return startString;
                            } else {
                                // Is not our hardString.
                                return -1;
                            }
                        }

                        index++;
                    }

                    // Set start index for next iteration.
                    startIndex = endString + 1;
                } else {
                    startIndex = endLineIndex + 1;
                }
            } // End of infinite loop.

            return -1;
        }

        /**
         * Checks whether the given section of text overlaps with any of the
         * guarded sections in the editor.
         *
         * @param  startPos  beginning position if the section to check
         * @param  endPos    ending position of the section to check
         * @return  <code>true</code> if the section of text overlaps,
         *          <code>false</code> otherwise
         */
        private synchronized boolean isInGuardedSection(final Position startPos,
                                                        final Position endPos) {
            EditorCookie editor = sourceDataObject.getCookie(EditorCookie.class);
            StyledDocument doc = null;
            GuardedSectionManager guards = null;
            if (editor != null) {
                try {
                    doc = editor.openDocument();
                }
                catch (IOException ex) {
                    Logger.getLogger("global").log(Level.SEVERE, ex.getLocalizedMessage(), ex);
                    return false;
                }
            }
            
            if (doc != null) {
                guards = GuardedSectionManager.getInstance(doc);
            }
            
            if (guards != null) {
                for (Iterator it = guards.getGuardedSections().iterator(); it.hasNext();) {
                    GuardedSection gsection = (GuardedSection) it.next();
                    if (gsection.contains(startPos, true) ||
                            gsection.contains(endPos, true)) {
                        return true;
                    }
                }

            }
            return false;
        }

        DataObject getSourceDataObject() {
	  return sourceDataObject;
	}


    } // End of nested class I18nFormFinder.

    
    /** Replacer used by enclosing class. */
   private static class FormI18nReplacer extends JavaI18nReplacer {
       
       /** Reference to form finder. */
       private FormI18nFinder finder;
       
       private final ResourceBundle bundle;
       
       /** Constructor. */
       public FormI18nReplacer(FormI18nFinder finder) {
           this.finder = finder;
           bundle = NbBundle.getBundle(FormI18nSupport.class);
       }

       
       @Override
       public void replace(final HardCodedString hcString, final I18nString i18nString) {
           if(hcString instanceof FormHardCodedString) {
               replaceInGuarded((FormHardCodedString)hcString, (JavaI18nString)i18nString);
           } else {
               super.replace(hcString, i18nString);
           }
       }

       /** Replaces found hard coded string in guarded blocks. */
       private void replaceInGuarded(FormHardCodedString formHcString, JavaI18nString javaI18nString) {
           try {
               String replaceString = javaI18nString.getReplaceString();

               // Remember position offset before change of guarded block
               int lastPos = finder.getLastPosition().getOffset();

               int pos = formHcString.getEndPosition().getOffset();

               // new value to set
               Object newValue;

               Node.Property nodeProperty = formHcString.getNodeProperty();

               ValidFormProperty validProp = formHcString.getValidProperty();

               // old value
               Object oldValue = nodeProperty.getValue();

               // RAD property -> like text, title etc.
               if(nodeProperty instanceof FormProperty) {
                   if(oldValue instanceof RADConnectionDesignValue
                   && ((RADConnectionDesignValue)oldValue).getType() == RADConnectionDesignValue.TYPE_CODE) {
                       // The old value is set via RADConnectionPropertyEditor,
                       // (in our case if value was RADConnectionDesignValue of type TYPE_CODE (= user code))
                       String oldString = ((RADConnectionDesignValue)oldValue).getCode();

                       StringBuffer buff = new StringBuffer(oldString);

                       int index = indexOfHardString(oldString, formHcString.getText(), validProp.getSkip());

                       if (index == -1) {
                           NotifyDescriptor.Message message = new NotifyDescriptor.Message(
                               bundle.getString("MSG_StringNotFoundInGuarded"), NotifyDescriptor.ERROR_MESSAGE);
                               DialogDisplayer.getDefault().notify(message);
                                
                           return;
                       }

                       int startOffset = index;

                       int endOffset = startOffset + formHcString.getText().length() + 2; // 2 for quotes.

                       buff.replace(startOffset, endOffset, replaceString);

                       RADConnectionDesignValue newConnectionValue = new RADConnectionDesignValue(buff.toString());
                       newValue = newConnectionValue;
                   } else {
                       // The old value is set via ResourceBundleStringFormEditor,
                       // (in our case if value was "plain string" or
                       // RADConnectionDesignValue of type TYPE_VALUE.
                       ((FormProperty)nodeProperty).setCurrentEditor(new FormI18nStringEditor());
                       newValue = new FormI18nString(javaI18nString);
                   }
               } else {
                   // Node.Property -> code generation properties.
                   // Replace the part of old value which matches "quoted" hardString only.
                   String oldString = (String)oldValue;
                   StringBuffer buff = new StringBuffer(oldString);

                   int index = indexOfHardString(oldString, formHcString.getText(), validProp.getSkip());

                   if(index == -1) {
                       NotifyDescriptor.Message message = new NotifyDescriptor.Message(
                           bundle.getString("MSG_StringNotFoundInGuarded"), NotifyDescriptor.ERROR_MESSAGE);
                           DialogDisplayer.getDefault().notify(message);
                           
                       return;
                   }

                   int startOffset = index;
                   
                   int endOffset = startOffset + formHcString.getText().length() + 2; // 2 for quotes

                   buff.replace(startOffset, endOffset, replaceString);

                   newValue = buff.toString();
               }

               // Finally set the new value to property.
               nodeProperty.setValue(newValue);

               // Decrement last found skip.
               finder.decrementLastFoundSkip();
               
               final StyledDocument document = javaI18nString.getSupport().getDocument();

               // form editor may change the string, e.g. replace ResourceBundle.getBundle with bundle variable
               int replaceStringLength = newValue instanceof FormI18nString ? 0 : replaceString.length();
               final int lastP = lastPos + replaceStringLength - formHcString.getText().length() - 2; // 2 for quotes
               SwingUtilities.invokeLater(new Runnable() {
                    public void run() {
                       // Little trick to reset the last position in finder after guarded block was regenerated.
                       if (document instanceof AbstractDocument) {
                            ((AbstractDocument)document).readLock();
                       }
                       try {
                           finder.setLastPosition(document.createPosition(lastP));                   
                       } catch (BadLocationException ble) {
                           ErrorManager.getDefault().notify(ErrorManager.INFORMATIONAL, ble);
                       } finally {
                           if (document instanceof AbstractDocument) {
                                ((AbstractDocument) document).readUnlock();
                           }
                       }
                    }
               });
            } catch (IllegalAccessException iae) {
                ErrorManager.getDefault().notify(ErrorManager.INFORMATIONAL, iae);
            } catch (InvocationTargetException ite) {
                ErrorManager.getDefault().notify(ErrorManager.INFORMATIONAL, ite);
            }
        }   

        /** Helper method. */
        private int indexOfHardString(String source, String hardString, int skip) {
            while (skip >= 0) {
                int index = finder.indexOfNonI18nString(source, hardString, skip);
                if (index >= 0) {
                    return index;
                }
                skip--;
            }
            
            return -1;
        }
        
    } // End of nested class FormI18nReplacer
    
    
    /** HardCoded string found within form guarded block
     * and which contains values of form property to wich belongs. */
    private static class FormHardCodedString extends HardCodedString {
        
        /** Valid property with name etc. */
        private ValidFormProperty validProperty;
        
        /** Valid property. */
        private Node.Property nodeProperty;
        

        /** Constructor. */
        FormHardCodedString(String text, Position startPosition, Position endPosition, 
            ValidFormProperty validProperty, Node.Property nodeProperty) {
                
            super(text, startPosition, endPosition);
            this.validProperty = validProperty;
            this.nodeProperty = nodeProperty;
        }
        
        
        /** Getter for <code>validProperty</code>. */
        public ValidFormProperty getValidProperty() {
            return validProperty;
        }
        
        /** Getter for <code>nodeProperty</code>. */
        public Node.Property getNodeProperty() {
            return nodeProperty;
        }
        
    } // End of nested class FormHardCodedString.

    
    /** Panel for showing info about hard coded string. */
    private static class FormInfoPanel extends InfoPanel {
        
        /** Constructor. */
        public FormInfoPanel(HardCodedString hcString, StyledDocument document) {
            super(hcString, document);
        }
        
        
        /** Implements superclass abstract method. */
        protected void setHardCodedString(HardCodedString hcString, StyledDocument document) {

            getStringText().setText(hcString == null ? "" : hcString.getText()); // NOI18N
            
            int pos;

            String hardLine;
            
            if (hcString.getStartPosition() == null) {
                hardLine = ""; // NOI18N
            } else {
                pos = hcString.getStartPosition().getOffset();

                try {
                    Element paragraph = document.getParagraphElement(pos);
                    hardLine = document.getText(paragraph.getStartOffset(), paragraph.getEndOffset()-paragraph.getStartOffset()).trim();
                } catch (BadLocationException ble) {
                    hardLine = ""; // NOI18N
                }
            }

            getFoundInText().setText(hardLine);

            if(hcString instanceof FormHardCodedString) {
                getComponentText().setText( ((FormHardCodedString)hcString).getValidProperty().getRADComponentName());
                getPropertyText().setText( ((FormHardCodedString)hcString).getNodeProperty().getName());
            } else {            
                remove(getComponentLabel());
                remove(getComponentText());
                remove(getPropertyLabel());
                remove(getPropertyText());
            }
            
        }
    } // End of FormInfoPanel inner class.

    /** Factory for <code>FormI18nSupport</code>. */
    @org.openide.util.lookup.ServiceProvider(service=org.netbeans.modules.i18n.I18nSupport.Factory.class)
    public static class Factory extends I18nSupport.Factory {

        /** Instantiated from META-INF/services */
        public Factory() {
        }

        /** Gets <code>I18nSupport</code> instance for specified data object and document.
         * @exception IOException when the document could not be loaded */
        @Override
        public I18nSupport create(DataObject dataObject) throws IOException {
            I18nSupport support = super.create(dataObject);
            
            FormDataObject formDataObject = (FormDataObject)dataObject;
            FormEditorSupport formSupport = (FormEditorSupport)formDataObject.getFormEditorSupport();
            if (formSupport.isOpened()) {
                return support;
            }
            if (formSupport.loadForm()) {
                return support;
            }
            throw new IOException("I18N: Loading form for " + dataObject.getName() + " was not succesful."); // NOI18N
        }
        
        /** Implements superclass abstract method. */
        public I18nSupport createI18nSupport(DataObject dataObject) {
            return new FormI18nSupport(dataObject);
        }
        
        /** Gets class of supported <code>DataObject</code>.
         * @return <code>FormDataObject</code> class or <code>null</code> */
        public Class getDataObjectClass() {
            return FormDataObject.class;
//            try {
//                // FIXME remove reflection with system classloader. what's wrong missing implementaion dependency?
//                return Class.forName("org.netbeans.modules.form..FormDataObject", false, (ClassLoader) Lookup.getDefault().lookup(ClassLoader.class));  // NOI18N
//            } catch (ClassNotFoundException e) {
//                ErrorManager.getDefault().log(ErrorManager.WARNING, "Cannot enable I18N support: " + e.getMessage());  //NOI18N
//                return null;
//            }
        }

    } // End of class Factory.

}
