/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright 1997-2010 Oracle and/or its affiliates. All rights reserved.
 *
 * Oracle and Java are registered trademarks of Oracle and/or its affiliates.
 * Other names may be trademarks of their respective owners.
 *
 * The contents of this file are subject to the terms of either the GNU
 * General Public License Version 2 only ("GPL") or the Common
 * Development and Distribution License("CDDL") (collectively, the
 * "License"). You may not use this file except in compliance with the
 * License. You can obtain a copy of the License at
 * http://www.netbeans.org/cddl-gplv2.html
 * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
 * specific language governing permissions and limitations under the
 * License.  When distributing the software, include this License Header
 * Notice in each file and include the License file at
 * nbbuild/licenses/CDDL-GPL-2-CP.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the GPL Version 2 section of the License file that
 * accompanied this code. If applicable, add the following below the
 * License Header, with the fields enclosed by brackets [] replaced by
 * your own identifying information:
 * "Portions Copyrighted [year] [name of copyright owner]"
 *
 * Contributor(s):
 *
 * The Original Software is NetBeans. The Initial Developer of the Original
 * Software is Sun Microsystems, Inc. Portions Copyright 1997-2006 Sun
 * Microsystems, Inc. All Rights Reserved.
 *
 * If you wish your version of this file to be governed by only the CDDL
 * or only the GPL Version 2, indicate your decision by adding
 * "[Contributor] elects to include this software in this distribution
 * under the [CDDL or GPL Version 2] license." If you do not indicate a
 * single choice of license, a recipient has the option to distribute
 * your version of this file under either the CDDL, the GPL Version 2 or
 * to extend the choice of license to its licensees as provided above.
 * However, if you add GPL Version 2 code and therefore, elected the GPL
 * Version 2 license, then the option applies only if the new code is
 * made subject to such option by the copyright holder.
 */

package org.netbeans.modules.editor.errorstripe;

import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.lang.ref.Reference;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.WeakHashMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.text.JTextComponent;
import org.netbeans.api.editor.mimelookup.MimeLookup;
import org.netbeans.api.editor.mimelookup.MimePath;
import org.netbeans.editor.AnnotationDesc;
import org.netbeans.editor.AnnotationType;
import org.netbeans.editor.AnnotationType.Severity;
import org.netbeans.editor.Annotations;
import org.netbeans.editor.BaseDocument;
import org.netbeans.lib.editor.util.swing.DocumentUtilities;
import org.netbeans.modules.editor.errorstripe.privatespi.Mark;
import org.netbeans.modules.editor.errorstripe.privatespi.MarkProvider;
import org.netbeans.modules.editor.errorstripe.privatespi.MarkProviderCreator;
import org.netbeans.modules.editor.errorstripe.privatespi.Status;
import org.netbeans.spi.editor.errorstripe.UpToDateStatus;
import org.netbeans.spi.editor.errorstripe.UpToDateStatusProvider;
import org.netbeans.spi.editor.errorstripe.UpToDateStatusProviderFactory;
import org.netbeans.spi.editor.mimelookup.InstanceProvider;
import org.openide.ErrorManager;
import org.netbeans.modules.editor.errorstripe.apimodule.SPIAccessor;
import org.netbeans.spi.editor.mimelookup.MimeLocation;
import org.openide.cookies.InstanceCookie;
import org.openide.filesystems.FileObject;
import org.openide.loaders.DataObject;
import org.openide.util.NbCollections;
import org.openide.util.WeakListeners;

/**
 *
 * @author Jan Lahoda
 */
final class AnnotationViewDataImpl implements PropertyChangeListener, AnnotationViewData, Annotations.AnnotationsListener {
    
    private static final Logger LOG = Logger.getLogger(AnnotationViewDataImpl.class.getName());
    
    private static final String UP_TO_DATE_STATUS_PROVIDER_FOLDER_NAME = "UpToDateStatusProvider"; //NOI18N
    private static final String TEXT_BASE_PATH = "Editors/text/base/"; //NOI18N

    private AnnotationView view;
    private Reference<JTextComponent> paneRef;
    private BaseDocument document;
    
    private List<MarkProvider> markProviders = new ArrayList<MarkProvider>();
    private List<UpToDateStatusProvider> statusProviders = new ArrayList<UpToDateStatusProvider>();

    private List<PropertyChangeListener> markProvidersWeakLs = new ArrayList();
    private List<PropertyChangeListener> statusProvidersWeakLs = new ArrayList();
    
    private Collection<Mark> currentMarks = null;
    private SortedMap<Integer, List<Mark>> marksMap = null;

    private static WeakHashMap<String, Collection<? extends MarkProviderCreator>> mime2Creators = new WeakHashMap<String, Collection<? extends MarkProviderCreator>>();
    private static WeakHashMap<String, Collection<? extends UpToDateStatusProviderFactory>> mime2StatusProviders = new WeakHashMap<String, Collection<? extends UpToDateStatusProviderFactory>>();

    private static LegacyCrapProvider legacyCrap;
    
    private Annotations.AnnotationsListener weakL;
    
    /** Creates a new instance of AnnotationViewData */
    public AnnotationViewDataImpl(AnnotationView view, JTextComponent pane) {
        this.view = view;
        this.paneRef = new WeakReference<>(pane);
        this.document = null;
    }
    
    public void register(BaseDocument document) {
        this.document = document;
        
        JTextComponent pane = paneRef.get();
        if (pane != null) {
            gatherProviders(pane);
        }
        
        if (document != null) {
            if (weakL == null) {
                weakL = WeakListeners.create(Annotations.AnnotationsListener.class, this, document.getAnnotations());
                document.getAnnotations().addAnnotationsListener(weakL);
            }
        }
        
        clear();
    }
    
    public void unregister() {
        if (document != null && weakL != null) {
            document.getAnnotations().removeAnnotationsListener(weakL);
            weakL = null;
        }
        
        removeListenersFromStatusProviders();
        removeListenersFromMarkProviders();
        
        document = null;
    }

    public static void initProviders(String mimeType) {
        // Legacy mime path (text/base)
        MimePath legacyMimePath = MimePath.parse("text/base");
        legacyCrap = MimeLookup.getLookup(legacyMimePath).lookup(LegacyCrapProvider.class);
        lookupProviders(mimeType);
    }

    private static void lookupProviders(String mimeType) {
        MimePath mimePath = MimePath.parse(mimeType);
        // Mark providers
        mime2Creators.put(mimeType, MimeLookup.getLookup(mimePath).lookupAll(MarkProviderCreator.class));
        // Status providers
        mime2StatusProviders.put(mimeType, MimeLookup.getLookup(mimePath).lookupAll(UpToDateStatusProviderFactory.class));
    }
    
    private void gatherProviders(JTextComponent pane) {
        long start = System.currentTimeMillis();

        // Collect legacy mark providers
        List<MarkProvider> newMarkProviders = new ArrayList<MarkProvider>();
        if (legacyCrap != null) {
            createMarkProviders(legacyCrap.getMarkProviderCreators(), newMarkProviders, pane);
        }
        
        // Collect mark providers
        String mimeType = DocumentUtilities.getMimeType(pane);

        if (mimeType == null) {
            mimeType = pane.getUI().getEditorKit(pane).getContentType();
        }
        
        Collection<? extends MarkProviderCreator> creators = 
            mime2Creators.get(mimeType);

        if (creators == null) { //nothing for current mimeType, probably wrong init
            lookupProviders(mimeType);
            creators = mime2Creators.get(mimeType);
        }

        createMarkProviders(creators, newMarkProviders, pane);

        removeListenersFromMarkProviders();
        this.markProviders = newMarkProviders;
        addListenersToMarkProviders();

        
        // Collect legacy status providers
        List<UpToDateStatusProvider> newStatusProviders = new ArrayList<UpToDateStatusProvider>();
        if (legacyCrap != null) {
            createStatusProviders(legacyCrap.getUpToDateStatusProviderFactories(), newStatusProviders, pane);
        }
        
        // Collect status providers
        Collection<? extends UpToDateStatusProviderFactory> factories = 
            mime2StatusProviders.get(mimeType);
        if (factories != null) {
            createStatusProviders(factories, newStatusProviders, pane);
        } else {
            if (LOG.isLoggable(Level.FINE)) {
                LOG.fine("Factories set to null in mimeType " + mimeType); //NOI18N
            }
        }

        removeListenersFromStatusProviders();
        this.statusProviders = newStatusProviders;
        addListenersToStatusProviders();
        
        
        long end = System.currentTimeMillis();
        if (AnnotationView.TIMING_ERR.isLoggable(ErrorManager.INFORMATIONAL)) {
            AnnotationView.TIMING_ERR.log(ErrorManager.INFORMATIONAL, "gather providers took: " + (end - start));
        }
    }

    private static void createMarkProviders(Collection<? extends MarkProviderCreator> creators, List<MarkProvider> providers, JTextComponent pane) {
        for (MarkProviderCreator creator : creators) {
            if (LOG.isLoggable(Level.FINE)) {
                LOG.fine("creator = " + creator);
            }

            MarkProvider provider = creator.createMarkProvider(pane);
            if (provider != null) {
                providers.add(provider);
            }
        }
    }

    private static void createStatusProviders(Collection<? extends UpToDateStatusProviderFactory> factories, List<UpToDateStatusProvider> providers, JTextComponent pane) {
        for(UpToDateStatusProviderFactory factory : factories) {
            if (LOG.isLoggable(Level.FINE)) {
                LOG.fine("factory = " + factory);
            }

            UpToDateStatusProvider provider = factory.createUpToDateStatusProvider(pane.getDocument());
            if (provider != null) {
                providers.add(provider);
            }
        }
    }
    
    private void addListenersToStatusProviders() {
        for (Iterator p = statusProviders.iterator(); p.hasNext(); ) {
            UpToDateStatusProvider provider = (UpToDateStatusProvider) p.next();
            
            // removePropertyChangeListener() is non-public but present in UpToDateStatusProvider - will the weak listener removal work??
            PropertyChangeListener weakL = WeakListeners.propertyChange(this, provider);
            SPIAccessor.getDefault().addPropertyChangeListener(provider, weakL);
            markProvidersWeakLs.add(weakL);
        }
    }
        
    private void addListenersToMarkProviders() {
        for (Iterator p = markProviders.iterator(); p.hasNext(); ) {
            MarkProvider provider = (MarkProvider) p.next();
            
            PropertyChangeListener weakL = WeakListeners.propertyChange(this, provider);
            provider.addPropertyChangeListener(weakL);
            statusProvidersWeakLs.add(weakL);
        }
    }

    private void removeListenersFromStatusProviders() {
        if (statusProvidersWeakLs.size() == statusProviders.size()) { // Check if the listeners were not removed already
            int lIndex = 0;
            for (UpToDateStatusProvider statusProvider : statusProviders) {
                SPIAccessor.getDefault().removePropertyChangeListener(statusProvider, statusProvidersWeakLs.get(lIndex));
            }
        }
        statusProvidersWeakLs.clear();
    }
        
    private void removeListenersFromMarkProviders() {
        if (markProvidersWeakLs.size() == markProviders.size()) { // Check if the listeners were not removed already
            int lIndex = 0;
            for (MarkProvider markProvider : markProviders) {
                markProvider.removePropertyChangeListener(markProvidersWeakLs.get(lIndex));
            }
        }
        markProvidersWeakLs.clear();
    }
    
    /*package private*/ static Collection<Mark> createMergedMarks(List<MarkProvider> providers) {
        Collection<Mark> result = new LinkedHashSet<Mark>();
        
        for(MarkProvider provider : providers) {
            result.addAll(provider.getMarks());
        }
        
        return result;
    }
    
    /*package private for tests*/synchronized Collection<Mark> getMergedMarks() {
        if (currentMarks == null) {
            currentMarks = createMergedMarks(markProviders);
        }
        
        return new ArrayList<Mark>(currentMarks);
    }
    
    /*package private*/ static List<Mark> getStatusesForLineImpl(int line, SortedMap<Integer, List<Mark>> marks) {
        List<Mark> inside = marks.get(line);
        return inside == null ? Collections.<Mark>emptyList() : inside;
    }
    
    public Mark getMainMarkForBlock(int startLine, int endLine) {
        Mark m1;
        synchronized(this) {
            m1 = getMainMarkForBlockImpl(startLine, endLine, getMarkMap());
        }
        Mark m2 = getMainMarkForBlockAnnotations(startLine, endLine);
        
        if (m1 == null)
            return m2;
        
        if (m2 == null)
            return m1;
        
        if (isMoreImportant(m1, m2))
            return m1;
        else
            return m2;
    }
    
    /*package private*/ static Mark getMainMarkForBlockImpl(int startLine, int endLine, SortedMap<Integer, List<Mark>> marks) {
        int current = startLine - 1;
        Mark found = null;
        
        while ((current = findNextUsedLine(current, marks)) != Integer.MAX_VALUE && current <= endLine) {
            for (Mark newMark : getStatusesForLineImpl(/*doc, */current, marks)) {
                if (found == null || isMoreImportant(newMark, found)) {
                    found = newMark;
                }
            }
        }
        
        return found;
    }
    
    private static boolean isMoreImportant(Mark m1, Mark m2) {
        int compared = m1.getStatus().compareTo(m2.getStatus());
        
        if (compared == 0)
            return m1.getPriority() < m2.getPriority();
        
        return compared > 0;
    }
    
    private boolean isMoreImportant(AnnotationDesc a1, AnnotationDesc a2) {
        AnnotationType t1 = a1.getAnnotationTypeInstance();
        AnnotationType t2 = a2.getAnnotationTypeInstance();
        
        int compared = t1.getSeverity().compareTo(t2.getSeverity());
        
        if (compared == 0)
            return t1.getPriority() < t2.getPriority();
        
        return compared > 0;
    }
    
    private boolean isValidForErrorStripe(AnnotationDesc a) {
        return a.getAnnotationTypeInstance().getSeverity() != AnnotationType.Severity.STATUS_NONE;
    }
    
    private Mark getMainMarkForBlockAnnotations(int startLine, int endLine) {
        AnnotationDesc foundDesc = null;

        for (AnnotationDesc desc : NbCollections.iterable(listAnnotations(startLine, endLine))) {
            if ((foundDesc == null || isMoreImportant(desc, foundDesc)) && isValidForErrorStripe(desc))
                foundDesc = desc;
        }
        
        if (foundDesc != null)
            return new AnnotationMark(foundDesc);
        else
            return null;
    }

    public int findNextUsedLine(int from) {
        int line1;
        synchronized (this) {
            line1 = findNextUsedLine(from, getMarkMap());
        }

        int line2 = document.getAnnotations().getNextLineWithAnnotation(from + 1);
        
        if (line2 == (-1))
            line2 = Integer.MAX_VALUE;
        
        return line1 < line2 ? line1 : line2;
    }
    
    /*package private*/ static int findNextUsedLine(int from, SortedMap<Integer, List<Mark>> marks) {
        SortedMap<Integer, List<Mark>> next = marks.tailMap(from + 1);
        
        if (LOG.isLoggable(Level.FINE)) {
            LOG.fine("AnnotationView.findNextUsedLine from: " + from + "; marks: " + marks + "; next: " + next); //NOI18N
        }
        
        if (next.isEmpty()) {
            return Integer.MAX_VALUE;
        }
        
        return next.firstKey().intValue();
    }
    
    private void registerMark(Mark mark) {
        int[] span = mark.getAssignedLines();
        
        if (LOG.isLoggable(Level.FINE)) {
            LOG.fine("AnnotationView.registerMark mark: " + mark + "; from-to: " + span[0] + "-" + span[1]); //NOI18N
        }
        
        for (int line = span[0]; line <= span[1]; line++) {
            List<Mark> inside = marksMap.get(line);
            
            if (inside == null) {
                inside = new ArrayList<Mark>();
                marksMap.put(line, inside);
            }
            
            inside.add(mark);
        }
    }
    
    private void unregisterMark(Mark mark) {
        int[] span = mark.getAssignedLines();
        
        if (LOG.isLoggable(Level.FINE)) {
            LOG.fine("AnnotationView.unregisterMark mark: " + mark + "; from-to: " + span[0] + "-" + span[1]); //NOI18N
        }
        
        for (int line = span[0]; line <= span[1]; line++) {
            List<Mark> inside = marksMap.get(line);
            
            if (inside != null) {
                inside.remove(mark);
                
                if (inside.size() == 0) {
                    marksMap.remove(line);
                }
            }
        }
    }
    
    /*package private for tests*/synchronized SortedMap<Integer, List<Mark>> getMarkMap() {
        if (marksMap == null) {
            Collection<Mark> marks = getMergedMarks();
            marksMap = new TreeMap<Integer, List<Mark>>();
            
            for (Mark mark : marks) {
                registerMark(mark);
            }
        }
        
        return marksMap;
    }

    @Override
    public Status computeTotalStatus() {
        Status targetStatus = Status.STATUS_OK;
        Collection<Mark> marks = getMergedMarks();
        
        for(Mark mark : marks) {
            Status s = mark.getStatus();
            targetStatus = Status.getCompoundStatus(s, targetStatus);
        }

        for (AnnotationDesc desc : NbCollections.iterable(listAnnotations(-1, Integer.MAX_VALUE))) {
            Status s = get(desc.getAnnotationTypeInstance());

            if (s != null)
                targetStatus = Status.getCompoundStatus(s, targetStatus);
        }
        
        return targetStatus;
    }
    
    @Override
    public UpToDateStatus computeTotalStatusType() {
        if (statusProviders.isEmpty())
            return UpToDateStatus.UP_TO_DATE_DIRTY;
        
        UpToDateStatus statusType = UpToDateStatus.UP_TO_DATE_OK;
        
        for (Iterator p = statusProviders.iterator(); p.hasNext(); ) {
            UpToDateStatusProvider provider = (UpToDateStatusProvider) p.next();
            UpToDateStatus newType = provider.getUpToDate();
            
            if (newType.compareTo(statusType) > 0) {
                statusType = newType;
            }
        }
        
        return statusType;
    }

    @Override
    public void propertyChange(PropertyChangeEvent evt) {
        if ("marks".equals(evt.getPropertyName())) {
            synchronized (this) {
                @SuppressWarnings("unchecked")
                Collection<Mark> nue = (Collection<Mark>) evt.getNewValue();
                @SuppressWarnings("unchecked")
                Collection<Mark> old = (Collection<Mark>) evt.getOldValue();
                
                if (nue == null && evt.getSource() instanceof MarkProvider)
                    nue = ((MarkProvider) evt.getSource()).getMarks();
                
                if (old != null && nue != null) {
                    Collection<Mark> added = new LinkedHashSet<Mark>(nue);
                    Collection<Mark> removed = new LinkedHashSet<Mark>(old);
                    
                    // own removeAll since indexof on HashSet is faster than on ArrayList and AbstractSet call indexof on smaller collection
                    for (Iterator<Mark> old_it = old.iterator(); old_it.hasNext();) {
                        added.remove(old_it.next());
                    }
                    for (Iterator<Mark> nue_it = nue.iterator(); nue_it.hasNext();) {
                        removed.remove(nue_it.next());   
                    }
                    
                    if (marksMap != null) {
                        for(Mark mark : removed) {
                            unregisterMark(mark);
                        }
                        
                        for(Mark mark : added) {
                            registerMark(mark);
                        }
                    }
                    
                    if (currentMarks != null) {
                        LinkedHashSet<Mark> copy = new LinkedHashSet<Mark>(currentMarks);
                        copy.removeAll(removed);
                        copy.addAll(added);
                        currentMarks = copy;
                    }
                    
                    view.fullRepaint();
                } else {
                    LOG.warning("For performance reasons, the providers should fill both old and new value in property changes. Problematic event: " + evt);
                    clear();
                    view.fullRepaint();
                }
                return ;
            }
        }
        
        if (UpToDateStatusProvider.PROP_UP_TO_DATE.equals(evt.getPropertyName())) {
            view.fullRepaint(false);
            return ;
        }
    }

    public synchronized void clear() {
        currentMarks = null;
        marksMap = null;
    }
    
    public int[] computeErrorsAndWarnings() {
        int errors = 0;
        int warnings = 0;
        Collection<Mark> marks = getMergedMarks();
        
        for(Mark mark : marks) {
            Status s = mark.getStatus();
            
            errors += s == Status.STATUS_ERROR ? 1 : 0;
            warnings += s == Status.STATUS_WARNING ? 1 : 0;
        }
        
        for (AnnotationDesc desc : NbCollections.iterable(listAnnotations(-1, Integer.MAX_VALUE))) {
            Status s = get(desc.getAnnotationTypeInstance());

            if (s != null) {
                errors += s == Status.STATUS_ERROR ? 1 : 0;
                warnings += s == Status.STATUS_WARNING ? 1 : 0;
            }
        }
        
        return new int[] {errors, warnings};
    }
    
    public void changedLine(int Line) {
        changedAll();
    }
    
    public void changedAll() {
        view.fullRepaint(false);
    }
    
    static Status get(Severity severity) {
        if (severity == Severity.STATUS_ERROR)
            return Status.STATUS_ERROR;
        if (severity == Severity.STATUS_WARNING)
            return Status.STATUS_WARNING;
        if (severity == Severity.STATUS_OK)
            return Status.STATUS_OK;
        
        return null;
    }
    
    static Status get(AnnotationType ann) {
        return get(ann.getSeverity());
    }

    private Iterator<? extends AnnotationDesc> listAnnotations(final int startLine, final int endLine) {
        final Annotations annotations = document.getAnnotations();

        return new Iterator<AnnotationDesc>() {
            private final List<AnnotationDesc> remaining = new ArrayList<AnnotationDesc>();
            private int line = startLine;
            private int last = (-1);
            private int unchagedLoops = 0;
            private boolean stop = false;
            @Override public boolean hasNext() {
                if (stop) return false;
                if (remaining.isEmpty()) {
                    if ((line = annotations.getNextLineWithAnnotation(line)) <= endLine && line != (-1)) {
                        if (last == line) {
                            unchagedLoops++;
                            if (unchagedLoops >= 100) {
                                LOG.log(Level.WARNING, "Please add the following info to https://netbeans.org/bugzilla/show_bug.cgi?id=188843 : Possible infinite loop in getMainMarkForBlockAnnotations, debug data: {0}, unchaged loops: {1}", new Object[]{annotations.toString(), unchagedLoops});
                                stop = true;
                                return false;
                            }
                        } else {
                            if (line < last) {
                                LOG.log(Level.WARNING, "Please add the following info to https://netbeans.org/bugzilla/show_bug.cgi?id=188843 : line < last: {0} < {1}", new Object[]{line, last});
                                stop = true;
                                return false;
                            }
                            last = line;
                            unchagedLoops = 0;
                        }

                        AnnotationDesc desc = annotations.getActiveAnnotation(line);

                        if (desc != null) {
                            remaining.add(desc);
                        }

                        if (annotations.getNumberOfAnnotations(line) > 1) {
                            AnnotationDesc[] descriptions = annotations.getPassiveAnnotationsForLine(line);

                            if (descriptions != null) {
                                remaining.addAll(Arrays.asList(descriptions));
                            }
                        }

                        line++;
                    }
                }

                return !(stop = remaining.isEmpty());
            }
            @Override public AnnotationDesc next() {
                if (hasNext()) {
                    return remaining.remove(0);
                } else {
                    throw new NoSuchElementException();
                }
            }
            @Override public void remove() {
                throw new UnsupportedOperationException("Not supported yet.");
            }
        };
    }
    
    // XXX: This is here to help to deal with legacy code
    // that registered stuff in text/base. The artificial text/base
    // mime type is deprecated and should not be used anymore.
    @MimeLocation(subfolderName=UP_TO_DATE_STATUS_PROVIDER_FOLDER_NAME, instanceProviderClass=LegacyCrapProvider.class)
    public static final class LegacyCrapProvider implements InstanceProvider {

        private final List<FileObject> instanceFiles;
        private List<MarkProviderCreator> creators;
        private List<UpToDateStatusProviderFactory> factories;
        
        public LegacyCrapProvider() {
            this(null);
        }

        public LegacyCrapProvider(List<FileObject> files) {
            this.instanceFiles = files;
        }
        
        public Collection<? extends MarkProviderCreator> getMarkProviderCreators() {
            if (creators == null) {
                computeInstances();
            }
            return creators;
        }

        public Collection<? extends UpToDateStatusProviderFactory> getUpToDateStatusProviderFactories() {
            if (factories == null) {
                computeInstances();
            }
            return factories;
        }
        
        public Object createInstance(List fileObjectList) {
            ArrayList<FileObject> textBaseFilesList = new ArrayList<FileObject>();

            for(Object o : fileObjectList) {
                FileObject fileObject = null;

                if (o instanceof FileObject) {
                    fileObject = (FileObject) o;
                } else {
                    continue;
                }

                String fullPath = fileObject.getPath();
                int idx = fullPath.lastIndexOf(UP_TO_DATE_STATUS_PROVIDER_FOLDER_NAME);
                assert idx != -1 : "Expecting files with '" + UP_TO_DATE_STATUS_PROVIDER_FOLDER_NAME + "' in the path: " + fullPath; //NOI18N

                String path = fullPath.substring(0, idx);
                if (TEXT_BASE_PATH.equals(path)) {
                    textBaseFilesList.add(fileObject);
                    if (LOG.isLoggable(Level.WARNING)) {
                        LOG.warning("The 'text/base' mime type is deprecated, please move your file to the root. Offending file: " + fullPath); //NOI18N
                    }
                }
            }

            return new LegacyCrapProvider(textBaseFilesList);
        }
        
        private void computeInstances() {
            ArrayList<MarkProviderCreator> newCreators = new ArrayList<MarkProviderCreator>();
            ArrayList<UpToDateStatusProviderFactory> newFactories = new ArrayList<UpToDateStatusProviderFactory>();
            
            for(FileObject f : instanceFiles) {
                if (!f.isValid() || !f.isData()) {
                    continue;
                }
                
                try {
                    DataObject d = DataObject.find(f);
                    InstanceCookie ic = d.getLookup().lookup(InstanceCookie.class);
                    if (ic != null) {
                        if (MarkProviderCreator.class.isAssignableFrom(ic.instanceClass())) {
                            MarkProviderCreator creator = (MarkProviderCreator) ic.instanceCreate();
                            newCreators.add(creator);
                        } else if (UpToDateStatusProviderFactory.class.isAssignableFrom(ic.instanceClass())) {
                            UpToDateStatusProviderFactory factory = (UpToDateStatusProviderFactory) ic.instanceCreate();
                            newFactories.add(factory);
                        }
                    }
                } catch (Exception e) {
                    LOG.log(Level.WARNING, null, e);
                }
            }
            
            this.creators = newCreators;
            this.factories = newFactories;
        }
    } // End of LegacyToolbarActionsProvider class
}
