/*
 * 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.css.visual;

import java.awt.Component;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.beans.PropertyEditor;
import java.beans.PropertyEditorSupport;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.logging.Level;
import javax.swing.DefaultCellEditor;
import javax.swing.JLabel;
import javax.swing.JTable;
import javax.swing.SwingUtilities;
import javax.swing.table.TableCellRenderer;
import javax.swing.text.BadLocationException;
import javax.swing.text.Document;
import org.netbeans.editor.BaseDocument;
import org.netbeans.editor.Utilities;
import org.netbeans.modules.css.lib.api.properties.FixedTextGrammarElement;
import org.netbeans.modules.css.lib.api.properties.GrammarElementVisitor;
import org.netbeans.modules.css.lib.api.properties.GroupGrammarElement;
import org.netbeans.modules.css.lib.api.properties.Properties;
import org.netbeans.modules.css.lib.api.properties.PropertyCategory;
import org.netbeans.modules.css.lib.api.properties.PropertyDefinition;
import org.netbeans.modules.css.lib.api.properties.ResolvedProperty;
import org.netbeans.modules.css.lib.api.properties.Token;
import org.netbeans.modules.css.lib.api.properties.UnitGrammarElement;
import org.netbeans.modules.css.model.api.*;
import org.netbeans.modules.css.visual.api.DeclarationInfo;
import org.netbeans.modules.parsing.api.Snapshot;
import org.openide.explorer.propertysheet.ExPropertyEditor;
import org.openide.explorer.propertysheet.PropertyEnv;
import org.openide.filesystems.FileObject;
import org.openide.nodes.AbstractNode;
import org.openide.nodes.Children;
import org.openide.nodes.Node;
import org.openide.nodes.PropertySupport;
import org.openide.util.Exceptions;
import org.openide.util.Lookup;
import org.openide.util.Mutex;
import org.openide.util.NbBundle;
import org.openide.util.RequestProcessor.Task;

/**
 * A node representing a CSS rule with no children. The node properties
 * represents the css rule properties.
 *
 * @author marekfukala
 */
@NbBundle.Messages({
    "rule.properties=Properties",
    "rule.properties.description=Properties of the css rule",
    "rule.properties.add.declaration.tooltip=Enter a value to add this property to the selected rule",
    "rule.global.set.displayname=All Categories",
    "rule.global.set.tooltip=Properties from All Categories"
})
public class RuleEditorNode extends AbstractNode {

    private static String COLOR_CODE_GRAY = "777777";
    private static String COLOR_CODE_RED = "ff7777";
    public static String NONE_PROPERTY_NAME = "<none>";
    private String filterText;
    private PropertySetsInfo propertySetsInfo;
    private RuleEditorPanel panel;
    private Map<PropertyDefinition, PropertyDeclaration> addedDeclarations = new HashMap<>();
    private Rule lastRule;
    
    //cache the model.canApplyChanges() as it is very costly operation
    private boolean readOnlyMode;

    public RuleEditorNode(RuleEditorPanel panel) {
        super(new RuleChildren());
        this.panel = panel;
    }

    public Model getModel() {
        return panel.getModel();
    }
    
    public boolean isReadOnlyMode() {
        return readOnlyMode;
    }

    public FileObject getFileObject() {
        return getModel().getLookup().lookup(FileObject.class);
    }

    public Rule getRule() {
        return panel.getRule();
    }
    
    public boolean isShowAllProperties() {
        return panel.getViewMode().isShowAllProperties();
    }

    public boolean isShowCategories() {
        return panel.getViewMode().isShowCategories();
    }

    public boolean isAddPropertyMode() {
        return panel.isAddPropertyMode();
    }

    //called by the RuleEditorPanel when user types into the filter text field
    void setFilterText(String prefix) {
        this.filterText = prefix;
        fireContextChanged(true); //recreate the property sets
    }

    //called by the RuleEditorPanel when any of the properties affecting 
    //the PropertySet-s generation changes.
    public void fireContextChanged(boolean forceRefresh) {
        boolean oldReadOnlyModel = readOnlyMode;
        readOnlyMode = getModel() == null || !getModel().canApplyChanges();
        if(oldReadOnlyModel != readOnlyMode) {
            //refresh the PS as the read only mode changes
            forceRefresh = true;
        }
        
        try {
            PropertySetsInfo oldInfo = getCachedPropertySetsInfo();
            PropertySetsInfo newInfo = createPropertySetsInfo();
            
            PropertyCategoryPropertySet[] oldSets = oldInfo.getSets();
            PropertyCategoryPropertySet[] newSets = newInfo.getSets();

            if (!forceRefresh) {
                //the client doesn't require the property sets to be really recreated,
                //we may try to update them only if possible

                //compare old and new sets, if they contain same sets with same properties,
                //then update the PropertyDefinition-s so they contain reference to the current
                //css model vertion.
                //
                //if there's a new PropertySet or one of the PropertySets contains more or less
                //properties than the original, then do not do the incremental update but
                //refresh the PropertySets completely.
                update:
                {
                    //check if the "created declaration" flag has changed or not
                    if(oldInfo.isCreatedDeclaration() != newInfo.isCreatedDeclaration()) {
                        break update; //dpn't merge
                    }

                    //old DeclarationProperty to new value map
                    if (oldSets.length == newSets.length) {
                        for (int i = 0; i < oldSets.length; i++) {
                            PropertyCategoryPropertySet o = oldSets[i];
                            PropertyCategoryPropertySet n = newSets[i];

                            Map<PropertyDeclaration, DeclarationProperty> om = o.declaration2PropertyMap;
                            Map<PropertyDeclaration, DeclarationProperty> nm = n.declaration2PropertyMap;

                            if (om.size() != nm.size()) {
                                break update;
                            }
                            //same number of declarations

                            //notice: the same order of the properties as in the last model
                            //is ensured by the getUniquePropertyName() method which adds 
                            //index of the property in the rule to its name.

                            //create declaration name -> declaration maps se we may compare 
                            //(as the css source model elements do not comparable by equals/hashcode)
                            Map<String, PropertyDeclaration> oName2DeclarationMap = new HashMap<>();
                            for (PropertyDeclaration d : om.keySet()) {
                                if (lastRule.getModel() != d.getModel()) {
                                    break update; // Issue 234155
                                }
                                oName2DeclarationMap.put(PropertyUtils.getDeclarationId(lastRule, d), d);
                            }
                            Map<String, PropertyDeclaration> nName2DeclarationMap = new HashMap<>();
                            for (PropertyDeclaration d : nm.keySet()) {
                                nName2DeclarationMap.put(PropertyUtils.getDeclarationId(getRule(), d), d);
                            }

                            //compare the names of the properties in the old and new map,
                            //they must be the same otherwise we wont' marge but recreate 
                            //the whole property sets
                            Collection<String> oldNames = oName2DeclarationMap.keySet();
                            Collection<String> newNames = nName2DeclarationMap.keySet();
                            Collection<String> comp = new HashSet<>(oldNames);
                            if (comp.retainAll(newNames)) { //assumption: the collections size are the same
                                break update; //canot merge - the collections differ
                            }

                            for (String declarationName : oName2DeclarationMap.keySet()) {
                                PropertyDeclaration oldD = oName2DeclarationMap.get(declarationName);
                                PropertyDeclaration newD = nName2DeclarationMap.get(declarationName);

                                //update the existing DeclarationProperty with the fresh
                                //Declaration object from the new model instance
                                DeclarationProperty declarationProperty = om.get(oldD);
                                declarationProperty.updateDeclaration(newD);

                                //also update the declaration2PropertyMap itself 
                                //as we now use new Declaration object
                                om.remove(oldD);
                                om.put(newD, declarationProperty);
                            }

                        }
                        return;
                    }

                }
            }

            //refresh the sets completely
            propertySetsInfo = newInfo;
            firePropertySetsChange(oldSets, newSets);
        } finally {
            this.lastRule = getRule();
        }
    }

    void fireDeclarationInfoChanged(PropertyDeclaration declaration, DeclarationInfo declarationInfo) {
        DeclarationProperty dp = getDeclarationProperty(declaration);
        if (dp != null) {
            dp.setDeclarationInfo(declarationInfo);
        }
    }

    DeclarationProperty getDeclarationProperty(PropertyDeclaration declaration) {
        for (PropertyCategoryPropertySet set : getCachedPropertySetsInfo().getSets()) {
            DeclarationProperty declarationProperty = set.getDeclarationProperty(declaration);
            if (declarationProperty != null) {
                return declarationProperty;
            }
        }
        return null;
    }

    @Override
    public synchronized PropertySet[] getPropertySets() {
        this.lastRule = getRule();
        return getCachedPropertySetsInfo().getSets();
    }

    private synchronized PropertySetsInfo getCachedPropertySetsInfo() {
        if (propertySetsInfo == null) {
            propertySetsInfo = createPropertySetsInfo();
        }
        return propertySetsInfo;
    }

    private boolean matchesFilterText(String text) {
        if (filterText == null) {
            return true;
        } else {
            return text.contains(filterText);
        }
    }

    private Collection<PropertyDefinition> filterByPrefix(Collection<PropertyDefinition> defs) {
        Collection<PropertyDefinition> filtered = new ArrayList<>();
        for (PropertyDefinition pd : defs) {
            if (matchesFilterText(pd.getName())) {
                filtered.add(pd);
            }
        }
        return filtered;
    }

    /**
     * Creates property sets of the node.
     *
     * @return property sets of the node.
     */
    private PropertySetsInfo createPropertySetsInfo() {
        if (getModel() == null || getRule() == null) {
            return new PropertySetsInfo(new PropertyCategoryPropertySet[0], panel.getCreatedDeclaration() != null);
        }
        Collection<PropertyCategoryPropertySet> sets = new ArrayList<>();
        List<PropertyDeclaration> declarations = PropertyUtils.getPropertyDeclarations(getRule());

        FileObject file = getFileObject();

        if (isShowCategories()) {
            //create property sets for property categories

            Map<PropertyDefinition, PropertyDeclaration> created = new HashMap<>();
            Map<PropertyCategory, List<PropertyDeclaration>> categoryToDeclarationsMap = new EnumMap<>(PropertyCategory.class);
            for (PropertyDeclaration d : declarations) {
                if (addedDeclarations.containsValue(d)) {
                    continue; //skip those added declarations
                }
                //check the declaration
                org.netbeans.modules.css.model.api.Property property = d.getProperty();
                PropertyValue propertyValue = d.getPropertyValue();
                if (property != null && propertyValue != null) {
                    if (matchesFilterText(property.getContent().toString())) {
                        PropertyDefinition def = Properties.getPropertyDefinition(property.getContent().toString());
                        
                        String declarationId = PropertyUtils.getDeclarationId(getRule(), d);
                        if(panel.getCreatedDeclarationsIdsList().contains(declarationId)) {
                            created.put(def, d);
                        }
                        
                        PropertyCategory category;
                        if (def != null) {
                            category = def.getPropertyCategory();
                        } else {
                            category = PropertyCategory.UNKNOWN;
                        }

                        List<PropertyDeclaration> values = categoryToDeclarationsMap.get(category);
                        if (values == null) {
                            values = new LinkedList<>();
                            categoryToDeclarationsMap.put(category, values);
                        }
                        values.add(d);
                    }
                }
            }

            Map<PropertyCategory, PropertyCategoryPropertySet> propertySetsMap = new EnumMap<>(PropertyCategory.class);
            for (Entry<PropertyCategory, List<PropertyDeclaration>> entry : categoryToDeclarationsMap.entrySet()) {

                List<PropertyDeclaration> categoryDeclarations = entry.getValue();
                
                if(isShowAllProperties()) {
                    //remove the "just created"
                    categoryDeclarations.removeAll(created.values());
                }
                
                //sort alpha
                Collections.sort(categoryDeclarations, PropertyUtils.getDeclarationsComparator());

                PropertyCategoryPropertySet propertyCategoryPropertySet = new PropertyCategoryPropertySet(entry.getKey());
                propertyCategoryPropertySet.addAll(categoryDeclarations);

                propertySetsMap.put(entry.getKey(), propertyCategoryPropertySet);
                sets.add(propertyCategoryPropertySet);
            }

            if (isShowAllProperties()) {
                //Show all properties
                for (PropertyCategory cat : PropertyCategory.values()) {
                    //now add all the remaining properties
                    List<PropertyDefinition> allInCat = new LinkedList<>(filterByPrefix(getCategoryProperties(cat)));
                    if (allInCat.isEmpty()) {
                        continue; //skip empty categories (when filtering)
                    }
                    Collections.sort(allInCat, PropertyUtils.getPropertyDefinitionsComparator());

                    PropertyCategoryPropertySet propertySet = propertySetsMap.get(cat);
                    if (propertySet == null) {
                        propertySet = new PropertyCategoryPropertySet(cat);
                        sets.add(propertySet);
                    }

                    //remove already used
                    for (PropertyDeclaration d : propertySet.getDeclarations()) {
                        PropertyDefinition def = Properties.getPropertyDefinition(d.getProperty().getContent().toString());
                        allInCat.remove(def);
                    }

                    //add the rest of unused properties to the property set
                    for (PropertyDefinition pd : allInCat) {
                        PropertyDeclaration alreadyAdded = addedDeclarations.get(pd);
                        if(alreadyAdded == null) {
                            alreadyAdded = created.get(pd);
                        }
                        if (alreadyAdded != null) {
                            propertySet.add(alreadyAdded, true);
                        } else {
                            propertySet.add(file, pd);
                        }
                    }

                }

            }

        } else {
            //not showCategories
            //just create one top level property set for virtual category (the items actually don't belong to the category)
            PropertyCategoryPropertySet set = new PropertyCategoryPropertySet(PropertyCategory.DEFAULT);

            if(!isShowAllProperties()) {
                //set properties only view
                List<PropertyDeclaration> filtered = new ArrayList<>();
                for (PropertyDeclaration d : declarations) {
                    if (addedDeclarations.containsValue(d)) {
                        continue; //skip those added declarations
                    }
                    String declarationId = PropertyUtils.getDeclarationId(getRule(), d);
                    if(panel.getCreatedDeclarationsIdsList().contains(declarationId)) {
                        //created declaration--ignore filter
                        filtered.add(d);
                    } else {
                        //check the declaration
                        org.netbeans.modules.css.model.api.Property property = d.getProperty();
                        PropertyValue propertyValue = d.getPropertyValue();
                        if (property != null && propertyValue != null) {
                            if (matchesFilterText(property.getContent().toString())) {
                                filtered.add(d);
                            }
                        }
                    }
                }
                //sort aplha
                Comparator<PropertyDeclaration> comparator = PropertyUtils.createDeclarationsComparator(getRule(), panel.getCreatedDeclarationsIdsList());
                Collections.sort(filtered, comparator);
                set.addAll(filtered);
                
                //do NOT show all properties
                //Add the fake "Add Property" FeatureDescriptor at the end of the set
                if(!readOnlyMode && panel.getCreatedDeclaration() == null) {
                    //do not add the "Add Property" item when we are editing value of the just added property
                    set.add_Add_Property_FeatureDescriptor();
                }
            } else {
                //all properties view
                List<PropertyDeclaration> filteredExisting = new ArrayList<>();
                Map<PropertyDefinition, PropertyDeclaration> filteredCreated = new HashMap<>();
                for (PropertyDeclaration d : declarations) {
                    if (addedDeclarations.containsValue(d)) {
                        continue; //skip those added declarations
                    }
                    String declarationId = PropertyUtils.getDeclarationId(getRule(), d);
                    if(panel.getCreatedDeclarationsIdsList().contains(declarationId)) {
                        //created declaration--ignore filter
                        filteredCreated.put(d.getResolvedProperty().getPropertyDefinition(), d);
                    } else {
                        //check the declaration
                        org.netbeans.modules.css.model.api.Property property = d.getProperty();
                        PropertyValue propertyValue = d.getPropertyValue();
                        if (property != null && propertyValue != null) {
                            if (matchesFilterText(property.getContent().toString())) {
                                filteredExisting.add(d);
                            }
                        }
                    }
                }
                
                set.addAll(filteredExisting);
                
                List<PropertyDefinition> all = new ArrayList<>(filterByPrefix(Properties.getPropertyDefinitions(file, true)));
                Collections.sort(all, PropertyUtils.getPropertyDefinitionsComparator());

                //remove already used
                for (PropertyDeclaration d : set.getDeclarations()) {
                    PropertyDefinition def = Properties.getPropertyDefinition(d.getProperty().getContent().toString());
                    all.remove(def);
                }

                //add the rest of unused properties to the property set
                for (PropertyDefinition pd : all) {
                    //boz<i' gula's<:
                    PropertyDeclaration alreadyAdded = addedDeclarations.get(pd); //added in "ADD PROPERTY MODE"
                    if(alreadyAdded == null) {
                        alreadyAdded = filteredCreated.get(pd); //added in normal mode
                    }
                    
                    if (alreadyAdded != null) {
                        set.add(alreadyAdded, true);
                    } else {
                        set.add(file, pd);
                    }
                }
            }
            
            //overrride the default descriptions
            set.setDisplayName(Bundle.rule_global_set_displayname());
            set.setShortDescription(Bundle.rule_global_set_tooltip());
            
            sets.add(set);
        }



        return new PropertySetsInfo(sets.toArray(new PropertyCategoryPropertySet[0]), panel.getCreatedDeclaration() != null);
    }

    /**
     * Returns a list of *visible* properties with this category.
     */
    public List<PropertyDefinition> getCategoryProperties(PropertyCategory cat) {
        Collection<PropertyDefinition> defs = Properties.getPropertyDefinitions(getModel().getLookup().lookup(FileObject.class), true);
        List<PropertyDefinition> defsInCat = new ArrayList<>();
        for (PropertyDefinition d : defs) {
            if (d.getPropertyCategory() == cat) {
                defsInCat.add(d);
            }
        }
        return defsInCat;
    }

    private String getPropertyDisplayName(PropertyDeclaration declaration) {
        return declaration.getProperty().getContent().toString();
    }

    public void applyModelChanges() {
        final Model model = getModel();
        model.runReadTask(new Model.ModelTask() {
            @Override
            public void run(StyleSheet styleSheet) {
                try {
                    model.applyChanges();
                } catch (IOException | BadLocationException ex) {
                    Exceptions.printStackTrace(ex);
                }
            }
        });
    }

    class PropertyCategoryPropertySet extends PropertySet {

        private List<Property> properties = new ArrayList<>();
        private Map<PropertyDeclaration, DeclarationProperty> declaration2PropertyMap = new HashMap<>();

        public PropertyCategoryPropertySet(PropertyCategory propertyCategory) {
            super(propertyCategory.name(), //NOI18N
                    propertyCategory.getDisplayName(),
                    propertyCategory.getShortDescription());
        }

        public void add_Add_Property_FeatureDescriptor() {
            properties.add(create_Add_Property_Feature_Descriptor());
        }

        public void add(PropertyDeclaration declaration, boolean markAsModified) {
            DeclarationProperty property = createDeclarationProperty(declaration, markAsModified);
            declaration2PropertyMap.put(declaration, property);
            properties.add(property);
        }

        public void addAll(Collection<PropertyDeclaration> declarations) {
            for (PropertyDeclaration d : declarations) {
                add(d, false);
            }
        }

        public Collection<PropertyDeclaration> getDeclarations() {
            return declaration2PropertyMap.keySet();
        }

        public DeclarationProperty getDeclarationProperty(PropertyDeclaration declaration) {
            return declaration2PropertyMap.get(declaration);
        }

        public void add(FileObject context, PropertyDefinition propertyDefinition) {
            properties.add(createPropertyDefinitionProperty(context, propertyDefinition));
        }

        @Override
        public Property<String>[] getProperties() {
            return properties.toArray(new Property[]{});
        }
    }

    private Property createPropertyDefinitionProperty(FileObject context, PropertyDefinition definition) {
        PropertyDefinition pmodel = Properties.getPropertyDefinition(definition.getName());
        return new PropertyDefinitionProperty(definition, createPropertyValueEditor(context, pmodel, null, false));
    }

    private PropertyValuesEditor createPropertyValueEditor(FileObject context, PropertyDefinition pmodel, PropertyDeclaration declaration, boolean addNoneProperty) {
        final Collection<UnitGrammarElement> unitElements = new ArrayList<>();
        final Collection<FixedTextGrammarElement> fixedElements = new ArrayList<>();

        if (pmodel != null) {
            GroupGrammarElement rootElement = pmodel.getGrammarElement(context);

            rootElement.accept(new GrammarElementVisitor() {
                @Override
                public void visit(UnitGrammarElement element) {
                    unitElements.add(element);
                }

                @Override
                public void visit(FixedTextGrammarElement element) {
                    fixedElements.add(element);
                }
            });
        }

        return new PropertyValuesEditor(panel, pmodel, getModel(), fixedElements, unitElements, declaration, addNoneProperty);

    }

    private abstract class AbstractPDP<T> extends PropertySupport<T> {

        private PropertyDefinition def;
        private PropertyEditor editor;

        public AbstractPDP(PropertyDefinition def, PropertyEditor editor, String name, Class<T> type, String displayName, String shortDescription, boolean canR, boolean canW) {
            super(name, type, displayName, shortDescription, canR, canW);
            this.def = def;
            this.editor = editor;
        }

        public AbstractPDP(PropertyDefinition def, PropertyEditor editor, Class<T> clazz, String shortDescription) {
            super(def.getName(),
                    clazz,
                    def.getName(),
                    shortDescription,
                    true,
                    getRule().isValid() && !readOnlyMode);
            this.def = def;
            this.editor = editor;
        }

        @Override
        public PropertyEditor getPropertyEditor() {
            return editor;
        }

        @Override
        public T getValue() throws IllegalAccessException, InvocationTargetException {
            return getEmptyValue();
        }

        @Override
        public void setValue(T val) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException {
            if (getEmptyValue().equals(val)) {
                return; //no change
            }

            //add a new declaration to the rule
            ElementFactory factory = getModel().getElementFactory();

            Rule rule = getRule();
            Declarations declarations = rule.getDeclarations();

            if (declarations == null) {
                //empty rule, create declarations node as well
                declarations = factory.createDeclarations();
                rule.setDeclarations(declarations);
            }

            org.netbeans.modules.css.model.api.Property property = factory.createProperty(def.getName());
            Expression expr = factory.createExpression(convertToString(val));
            PropertyValue value = factory.createPropertyValue(expr);
            PropertyDeclaration newPropertyDeclaration = factory.createPropertyDeclaration(property, value, false);
            Declaration newDeclaration = factory.createDeclaration();
            newDeclaration.setPropertyDeclaration(newPropertyDeclaration);
            declarations.addDeclaration(newDeclaration);

            //save the model to the source
            if (!isAddPropertyMode()) {
                panel.setCreatedDeclaration(rule, newPropertyDeclaration);
                applyModelChanges();
            } else {
                //add property mode - just refresh the content
                addedDeclarations.put(def, newPropertyDeclaration); //remember what we've added during this dialog cycle
                fireContextChanged(true);
            }
        }

        protected abstract String convertToString(T val);

        protected abstract T getEmptyValue();
    }
    private static String EMPTY_STRING = "";

    private class PlainPDP extends AbstractPDP<String> {

        public PlainPDP(PropertyDefinition def, PropertyEditor editor, String shortDescription) {
            super(def, editor, String.class, shortDescription);
        }

        @Override
        protected String convertToString(String val) {
            return val;
        }

        @Override
        protected String getEmptyValue() {
            return EMPTY_STRING;
        }
    }

    private class PropertyDefinitionProperty extends PlainPDP {

        public PropertyDefinitionProperty(PropertyDefinition def, PropertyEditor editor) {
            super(def,
                    editor,
                    Bundle.rule_properties_add_declaration_tooltip());
        }
    }

    private DeclarationProperty createDeclarationProperty(PropertyDeclaration declaration, boolean markAsModified) {
        ResolvedProperty resolvedProperty = declaration.getResolvedProperty();
        PropertyDefinition propertyDefinition = resolvedProperty != null ? resolvedProperty.getPropertyDefinition() : null;
        return new DeclarationProperty(declaration,
                PropertyUtils.getDeclarationId(getRule(), declaration),
                getPropertyDisplayName(declaration),
                markAsModified,
                createPropertyValueEditor(getFileObject(), propertyDefinition, declaration, true));
    }

    @NbBundle.Messages({
        "property.set.at.prefix=Set at ",
        "property.value.unexpected.token={0}, unexpected character(s) \"{1}\" found",
        "property.value.not.resolved={0}, error in property value",
        "property.erroneous={0}, erroneous property",
        "property.unknown={0}, unknown property",
        "property.inactive={0}, not affecting the selected element",
        "property.overridden={0}, overridden by another property",
        "property.description={0}",
        "property.no.file=No File"
    })
    public class DeclarationProperty extends PropertySupport {

        private final String propertyName;
        private final PropertyEditor editor;
        private final boolean markAsModified;
        private PropertyDeclaration propertyDeclaration;
        private DeclarationInfo info;
        private String shortDescription;
        private String valueSet;
        private String locationPrefix;

        public DeclarationProperty(PropertyDeclaration declaration, String propertyName, String propertyDisplayName, boolean markAsModified, PropertyEditor editor) {
            super(propertyName,
                    String.class,
                    propertyDisplayName,
                    null, true, !readOnlyMode && getRule().isValid());
            this.propertyName = propertyName;
            this.propertyDeclaration = declaration;
            this.markAsModified = markAsModified;
            this.editor = editor;

            checkForErrors();

            //one may set a custom inplace editor by 
            //setValue("inplaceEditor", new MyInplaceEditor());

        }
        
        public PropertyDeclaration getDeclaration() {
            return propertyDeclaration;
        }

        /**
         * Updates the {@link #info} field to {@link DeclarationInfo#ERRONEOUS}
         * if the active declaration contains errors.
         */
        private void checkForErrors() {
            //suppress the errors for just added property
            //it doesn't have the value yet, but this doesn't mean
            //we want to mark it as erroneous while adding the value
            if (getDeclaration().equals(panel.getCreatedDeclaration())) {
                return; 
            }

            String property = propertyDeclaration.getProperty().getContent().toString().trim();
            PropertyDefinition model = Properties.getPropertyDefinition(property);
            if (model == null) {
                //flag as unknown
                info = DeclarationInfo.ERRONEOUS;
                shortDescription = Bundle.property_unknown(getLocationPrefix());
                return ;
            }
             
            //so we have a property model...
            
            //but before checking the property value ensure we are not trying
            //to do so for vendor specific property. Values of these properties
            //are not supposed to be checked as the grammars are not very much
            //up-to-date and reliable.
            if(Properties.isVendorSpecificProperty(model)) {
                shortDescription = Bundle.property_description(getLocationPrefix());
                return ;
            }

            PropertyValue value = propertyDeclaration.getPropertyValue();
            if (value != null) {
                Expression expression = value.getExpression();
                CharSequence content = expression != null ? expression.getContent() : "";
                ResolvedProperty rp = new ResolvedProperty(getFileObject(), model, content);
                if (!rp.isResolved()) {
                    List<Token> unresolvedTokens = rp.getUnresolvedTokens();
                    if(unresolvedTokens.isEmpty()) {
                        //no value token/s
                        info = DeclarationInfo.ERRONEOUS;
                        shortDescription = Bundle.property_value_not_resolved(getLocationPrefix());
                        return ;
                    }
                    
                    //we have some unresolved token,
                    //lets check if the token is vendor specific value token
                    Token unexpectedToken = unresolvedTokens.iterator().next();
                    String unexpectedText = unexpectedToken.image().toString();
                    
                    if(!org.netbeans.modules.css.editor.module.spi.Utilities.isVendorSpecificPropertyValueToken(getFileObject(), unexpectedText)) {
                        //no, it seems to be a common value token
                        shortDescription = Bundle.property_value_unexpected_token(getLocationPrefix(), unexpectedText);
                        return;
                    }
                }
            }

            //else everything seems to be all right
            shortDescription = Bundle.property_description(getLocationPrefix());
        }

        /**
         * Returns the file:line prefix for the tooltip
         */
        private CharSequence getLocationPrefix() {
            if (locationPrefix == null) {
                final StringBuilder sb = new StringBuilder();
                sb.append(Bundle.property_set_at_prefix());
                Model model = getModel();
                Lookup lookup = model.getLookup();
                FileObject file = lookup.lookup(FileObject.class);
                if (file == null) {
                    sb.append(Bundle.property_no_file());
                } else {
                    sb.append(file.getNameExt());
                }
                Snapshot snap = lookup.lookup(Snapshot.class);
                final Document doc = lookup.lookup(Document.class);
                if (snap != null && doc != null) {
                    PropertyDeclaration decl = getDeclaration();
                    int ast_from = decl.getStartOffset();
                    if (ast_from != -1) {
                        //source element, not virtual which is not persisted yet
                        final int doc_from = snap.getOriginalOffset(ast_from);
                        if (doc_from != -1) {
                            doc.render(new Runnable() {
                                @Override
                                public void run() {
                                    try {
                                        int lineOffset = 1 + Utilities.getLineOffset((BaseDocument) doc, doc_from);
                                        sb.append(':');
                                        sb.append(lineOffset);
                                    } catch (BadLocationException ex) {
                                        //no-op
                                    }
                                }
                            });
                        }
                    }
                }
                locationPrefix = sb.toString();
            }

            return locationPrefix;
        }

        private void updateDeclaration(PropertyDeclaration declaration) {
            assert PropertyUtils.getDeclarationId(getRule(), declaration).equals(propertyName);

            //update the declaration
            String oldValue = getValue();
            this.propertyDeclaration = declaration;
            String newValue = getValue();

            locationPrefix = null; //reset the prefix as it was computed for the original declaration

            /* Reset DeclarationInfo to default state (null) as the contract 
             * doesn't require/expect the RuleEditorController.setDeclarationInfo(...) 
             * to be called for each "plain" declaration with null DeclarationInfo argument.
             */
            DeclarationInfo oldInfo = info;
            info = null;

            String oldShortDescription = shortDescription;

            //possibly set the DeclarationInfo to ERRONEOUS
            checkForErrors();

            if (!shortDescription.equals(oldShortDescription)) {
                fireShortDescriptionChange(oldShortDescription, shortDescription);
            }

            //now we need to fire property name property change with some 
            //change so call setDeclarationInfo() which does property change
            //from null to current value and hence forces the PS to repaint 
            //the property
            if (info != oldInfo) {
                //DeclarationInfo has changed
                setDeclarationInfo(info);
            } else {
                //no change to DeclarationInfo 
                setDisplayName(getHtmlDisplayName());
                //and fire property change to the node 
                //this will trigger the property name and value repaint
                firePropertyChange(propertyName, oldValue, newValue);
            }

        }

        @Override
        public String getShortDescription() {
            return shortDescription;
        }

        @Override
        public PropertyEditor getPropertyEditor() {
            return editor;
        }

        public void setDeclarationInfo(DeclarationInfo info) {
            if(this.info == info) {
                return ; //no change
            }
            
            this.info = info;
            setDisplayName(getHtmlDisplayName());
            
            //tooltip update
            String oldShortDescription = shortDescription;
            switch(info) {
                case ERRONEOUS:
                    shortDescription = Bundle.property_erroneous(getLocationPrefix());
                    break;
                case INACTIVE:
                    shortDescription = Bundle.property_inactive(getLocationPrefix());
                    break;
                case  OVERRIDDEN:
                    shortDescription = Bundle.property_overridden(getLocationPrefix());
                    break;
            }
            fireShortDescriptionChange(oldShortDescription, shortDescription);
            
            //force the property repaint - stupid way but there's
            //doesn't seem to be any better way
            firePropertyChange(propertyName, null, getValue());
        }

        private boolean isOverridden() {
            return info != null && info == DeclarationInfo.OVERRIDDEN;
        }

        private boolean isInactive() {
            return info != null && info == DeclarationInfo.INACTIVE;
        }

        private boolean isErroneous() {
            return info != null && info == DeclarationInfo.ERRONEOUS;
        }

        @Override
        public String getHtmlDisplayName() {
            StringBuilder b = new StringBuilder();

            String color = null;
            boolean bold = false;
            boolean strike = false;

            if (isShowAllProperties()) {
                if (isAddPropertyMode() && !markAsModified) {
                    color = COLOR_CODE_GRAY;
                } else {
                    bold = true;
                }
            }
            if (isOverridden()) {
                strike = true;
            }
            if (isInactive()) {
                color = COLOR_CODE_GRAY;
                strike = true;
            }
            if (isErroneous()) {
                strike = true;
                color = COLOR_CODE_RED;
            }

            //render
            if (bold) {
                b.append("<b>");//NOI18N
            }
            if (strike) {
                b.append("<s>"); //use <del>?
            }
            if (color != null) {
                b.append("<font color="); //NOI18N
                b.append(color);
                b.append(">"); //NOI18N
            }

            b.append(getPropertyDisplayName(propertyDeclaration));

            if (color != null) {
                b.append("</font>"); //NOI18N
            }
            if (strike) {
                b.append("</s>"); //use <del>?
            }
            if (bold) {
                b.append("</b>");//NOI18N
            }

            return b.toString();
        }

        @Override
        public String getValue() {
            if (valueSet != null) {
                return valueSet;
            }
            PropertyValue val = propertyDeclaration.getPropertyValue();
            return val == null ? null : val.getExpression().getContent().toString().trim();
        }

        @Override
        public void setValue(final Object o) {
            assert SwingUtilities.isEventDispatchThread();

            final String asString = (String) o;
            if (asString == null || asString.isEmpty()) {
                return;
            }
            String currentValue = getValue();
            if (asString.equals(currentValue)) {
                //same value, ignore
                return;
            }

            this.valueSet = asString;
            SAVE_CHANGE_TASK.schedule(200);

        }
        private Task SAVE_CHANGE_TASK = RuleEditorPanel.RP.create(new Runnable() {
            @Override
            public void run() {
                Mutex.EVENT.readAccess(new Runnable() {
                    @Override
                    public void run() {
                        //all the access to valueSet field is safe as 
                        //the field is only set in setValue() which always
                        //runs id EDT

                        //The tasks may schedule in such way that more than one tasks
                        //runs after the setValue(...) method called.
                        //In such case the first task sets the valueSet field to null
                        //and the other tasks cannot rule (they do not have anything
                        //to do anyway) so just quit in such case.
                        if (valueSet == null) {
                            return;
                        }

                        Model model = getModel();
                        model.runWriteTask(new Model.ModelTask() {
                            @Override
                            public void run(StyleSheet styleSheet) {
                                if (NONE_PROPERTY_NAME.equals(valueSet)) {
                                    //remove the whole declaration
                                    Declaration declaration = (Declaration) propertyDeclaration.getParent();
                                    Declarations declarations = (Declarations)declaration.getParent();
                                    declaration.removeElement(propertyDeclaration);
                                    declarations.removeDeclaration(declaration);
                                } else {
                                    //update the value
                                    RuleEditorPanel.LOG.log(Level.FINE, "updating property to {0}", valueSet);
                                    propertyDeclaration.getPropertyValue().getExpression().setContent(valueSet);
                                }
                            }
                        });

                        if (!isAddPropertyMode()) {
                            //save changes
                            applyModelChanges();

                            //the model save request will cause the source model's 
                            //Model.CHANGES_APPLIED_TO_DOCUMENT property change event fired
                            //and the RuleEditorPanel's listener will SYNCHRONOUSLY
                            //refresh the css source model.
                            //
                            //...so now we have a new instance of model reflecting
                            //the changes made by the writetask above
                        }

                        valueSet = null;
                    }
                });

            }
        });
    }

    private Property create_Add_Property_Feature_Descriptor() {
//        return ADD_PROPERTY_FD;
        //TODO put back the shared instance once Standa fixes the multiple setValue(...) calls so the innser property state can be removed.
        return new AddPropertyFD();
    }
    
    private Property ADD_PROPERTY_FD = new AddPropertyFD();

    @NbBundle.Messages({
        "AddProperty.displayName.html=<html><body><b>Add Property</b></body></html>",
        "AddProperty.displayName=Add Property",
        "AddProperty.shortDescription=Click here to add a new property."
    })
    
    private class AddPropertyFD extends Property<String> {

        private String valueSet;
        
        public AddPropertyFD() {
            super(String.class);
            setName(AddPropertyFD.class.getSimpleName());
            setDisplayName(Bundle.AddProperty_displayName());
            setShortDescription(Bundle.AddProperty_shortDescription());
        }

        @Override
        public PropertyEditor getPropertyEditor() {
            return new AddPropertyPropertyEditor(this);
        }

        @Override
        public boolean canRead() {
            return true;
        }

        @Override
        public String getValue() throws IllegalAccessException, InvocationTargetException {
            return "";
        }

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

        //called from AddPropertyPropertyEditor when a value is entered
        @Override
        public void setValue(final String propertyName) {
            if (propertyName == null) {
                return;
            }
            if(propertyName.trim().isEmpty()) {
                return ; //ignore no value
            }
            
            if(valueSet != null) {
                RuleEditorPanel.LOG.log(Level.WARNING, "Trying to set property value more than once!, relaxing...");
                return ;
            }
            valueSet = propertyName;

            
            //1.create the property
            //2.select the corresponding row in the PS
            
            final Model model = getModel();
            final Rule rule = getRule();
            model.runWriteTask(new Model.ModelTask() {
                @Override
                public void run(StyleSheet styleSheet) {
                    //add the new declaration to the model.
                    //the declaration is not complete - the value is missing and it is necessary to 
                    //enter in the PS otherwise the model become invalid.
                    ModelUtils utils = new ModelUtils(model);
                    Declarations decls = rule.getDeclarations();
                    if (decls == null) {
                        decls = model.getElementFactory().createDeclarations();
                        rule.setDeclarations(decls);
                    }

                    PropertyDeclaration pdeclarationElement = utils.createPropertyDeclaration(propertyName + ":");
                    Declaration declarationElement = model.getElementFactory().createDeclaration();
                    declarationElement.setPropertyDeclaration(pdeclarationElement);
                    decls.addDeclaration(declarationElement);

                    //do not save the model (apply changes) - once the write task finishes
                    //the embedded property sheet will be refreshed from the modified model.

                    //remember the created declaration so once the model change is fired
                    //and the property sheet is refreshed, we can find and select the corresponding
                    //FeatureDescriptor
                    panel.setCreatedDeclaration(rule, pdeclarationElement);
                }
            });
        }
    }

    private class AddPropertyPropertyEditor extends PropertyEditorSupport implements ExPropertyEditor {

        private AddPropertyFD property;

        public AddPropertyPropertyEditor(AddPropertyFD property) {
            this.property = property;
        }

        @Override
        public void attachEnv(PropertyEnv env) {
            env.getFeatureDescriptor().setValue("custom.cell.renderer", 
                    new AddPropertyCellRendererComponent()); //NOI18N
            
            env.getFeatureDescriptor().setValue("custom.cell.editor", 
                    new AddPropertyCellEditorComponent(new AutocompleteJComboBox(getFileObject()), property)); //NOI18N
        }
    }

    private class AddPropertyCellRendererComponent implements TableCellRenderer {

        @Override
        public Component getTableCellRendererComponent(JTable jtable, Object o, boolean bln, boolean bln1, int i, int i1) {
            return new JLabel(Bundle.AddProperty_displayName_html());
        }
    }

    private class AddPropertyCellEditorComponent extends DefaultCellEditor {

        private AutocompleteJComboBox editor;
        private AddPropertyFD property;
        
        private boolean cancelled;

        public AddPropertyCellEditorComponent(AutocompleteJComboBox jcb, AddPropertyFD addFDProperty) {
            super(jcb);
            this.property = addFDProperty;
            this.editor = jcb;
            this.editor.addActionListener(new ActionListener() {
                @Override
                public void actionPerformed(ActionEvent ae) {
                    if(!cancelled) {
                        property.setValue((String)editor.getSelectedItem());
                    }
                }
            });
            
        }
        @Override
        public Component getTableCellEditorComponent(JTable jtable, Object o, boolean bln, int i, int i1) {
            editor.setSelectedIndex( -1 );
            return editor;
        }

        @Override
        protected void fireEditingCanceled() {
            cancelled = true;
            super.fireEditingCanceled();
        }
        
    }

    /**
     * Empty children keys
     */
    private static class RuleChildren extends Children.Keys {

        @Override
        protected Node[] createNodes(Object key) {
            return new Node[]{};
        }
    }
    
    private static class PropertySetsInfo {
        
        private final PropertyCategoryPropertySet[] sets;
        private final boolean createdDeclaration; 

        public PropertySetsInfo(PropertyCategoryPropertySet[] sets, boolean createdDeclaration) {
            this.sets = sets;
            this.createdDeclaration = createdDeclaration;
        }

        public PropertyCategoryPropertySet[] getSets() {
            return sets;
        }

        /**
         * Returns true if the propertysets were created when there was 
         * "created declaration" set in the RuleEditorPanel.
         */
        public boolean isCreatedDeclaration() {
            return createdDeclaration;
        }
        
    }
}
