/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright 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]"
 * 
 * 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.
 * 
 * Contributor(s):
 * 
 * Portions Copyrighted 2008 Sun Microsystems, Inc.
 */

package org.netbeans.modules.parsing.impl;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.netbeans.api.annotations.common.NonNull;

import org.netbeans.api.editor.mimelookup.MimeLookup;
import org.netbeans.api.editor.mimelookup.MimePath;
import org.netbeans.modules.parsing.api.Embedding;
import org.netbeans.modules.parsing.api.Snapshot;
import org.netbeans.modules.parsing.api.Source;
import org.netbeans.modules.parsing.api.Task;
import org.netbeans.modules.parsing.spi.EmbeddingProvider;
import org.netbeans.modules.parsing.spi.ParseException;
import org.netbeans.modules.parsing.spi.Parser;
import org.netbeans.modules.parsing.spi.Parser.Result;
import org.netbeans.modules.parsing.spi.ParserFactory;
import org.netbeans.modules.parsing.spi.SchedulerEvent;
import org.netbeans.modules.parsing.spi.SchedulerTask;
import org.netbeans.modules.parsing.spi.SourceModificationEvent;
import org.netbeans.modules.parsing.spi.TaskFactory;
import org.netbeans.modules.parsing.spi.Scheduler;
import org.openide.filesystems.FileObject;
import org.openide.util.Lookup;
import org.openide.util.Pair;
import org.openide.util.WeakListeners;


/**
 * This class maintains cache of parser instance, snapshot and nested embeddings
 * for some block of code (or top level source).
 *
 * Threading: Instances of SourceCache and Source are synchronized using TaskProcessor.INTERNAL_LOCK
 * 
 * @author Jan Jancura
 */
//@ThreadSafe
public final class SourceCache {

    private static final Logger LOG = Logger.getLogger(SourceCache.class.getName());

    private static final Comparator<SchedulerTask> PRIORITY_ORDER = new Comparator<SchedulerTask>() {
        @Override
        public int compare(final SchedulerTask o1, final SchedulerTask o2) {
            final int p1 = o1.getPriority();
            final int p2 = o2.getPriority();
            return p1 < p2 ? -1 : p1 == p2 ? 0 : 1;
        }
    };
    
    private final Source    source;
    //@GuardedBy(this)
    private Embedding       embedding;
    private final String    mimeType;    

    public SourceCache (
        Source              source,
        Embedding           embedding
    ) {
        assert source != null;
        this.source = source;
        this.embedding = embedding;
        mimeType = embedding != null ?
            embedding.getMimeType () :
            source.getMimeType ();
        mimeType.getClass();
    }
    
    public SourceCache (
        Source              source,
        Embedding           embedding,
        Parser              parser
    ) {
        this(source, embedding);
        if (parser != null) {
            this.parserInitialized = true;
            this.parser = parser;
        }
    }

    private void setEmbedding (
        Embedding           embedding
    ) {
        synchronized (TaskProcessor.INTERNAL_LOCK) {
            assert embedding.getMimeType ().equals (mimeType);
            this.embedding = embedding;
            snapshot = null;
        }
    }
    //@GuardedBy(this)
    private Snapshot        snapshot;

    public Snapshot getSnapshot () {
        boolean isEmbedding;
        synchronized (TaskProcessor.INTERNAL_LOCK) {
            if (snapshot != null) {
                return snapshot;
            }
            isEmbedding = embedding != null;
        }

        final Snapshot _snapshot = createSnapshot (isEmbedding);
        synchronized (TaskProcessor.INTERNAL_LOCK) {
            if (snapshot == null) {
                snapshot = _snapshot;
            }
            return snapshot;
        }
    }
    
    public @NonNull Source getSource() {
        return this.source;
    }

    Snapshot createSnapshot (long[] idHolder) {
        assert idHolder != null;
        assert idHolder.length == 1;
        boolean isEmbedding;
        synchronized (TaskProcessor.INTERNAL_LOCK) {
            isEmbedding = embedding != null;
        }
        idHolder[0] = SourceAccessor.getINSTANCE ().getLastEventId (this.source);
        return createSnapshot (isEmbedding);
    }

    private Snapshot createSnapshot (boolean isEmbedding) {
        Snapshot _snapshot = isEmbedding ? embedding.getSnapshot () : source.createSnapshot ();
        assert mimeType.equals (_snapshot.getMimeType ());
        return _snapshot;
    }

    //@GuarderBy(this)
    private boolean         parserInitialized = false;
    //@GuardedBy(this)
    private Parser          parser;
    
    public Parser getParser () {
        synchronized (TaskProcessor.INTERNAL_LOCK) {
            if (parserInitialized) {
                return parser;
            }
        }
        Parser _parser = null;
        Lookup lookup = MimeLookup.getLookup (mimeType);
        ParserFactory parserFactory = lookup.lookup (ParserFactory.class);
        if (parserFactory != null) {
            final Snapshot _snapshot = getSnapshot ();
            final Collection<Snapshot> _tmp = Collections.singleton (_snapshot);
            _parser = parserFactory.createParser (_tmp);
            if (_parser == null) {
                LOG.log(
                    Level.INFO,
                    "Parser factory: {0} returned null parser for {1}", //NOI18N
                    new Object[]{
                        parserFactory,
                        _snapshot
                    });
            }
        }

        synchronized (TaskProcessor.INTERNAL_LOCK) {
            if (!parserInitialized) {                                                                                
                parser = _parser;
                if (parser != null) {
                    parser.addChangeListener(WeakListeners.change(
                        SourceAccessor.getINSTANCE().getParserEventForward(source),
                        parser));
                }
                parserInitialized = true;
            }
            return parser;
        }
    }
    
    //@GuardedBy(this)
    private boolean                     parsed = false;
    
    public Result getResult (
        final Task                      task
    ) throws ParseException {
        assert TaskProcessor.holdsParserLock();
        Parser _parser = getParser ();
        if (_parser == null) return null;
        boolean _parsed;
        synchronized (TaskProcessor.INTERNAL_LOCK) {
            _parsed = this.parsed;
            this.parsed = true; //Optimizstic update
        }
        if (!_parsed) {
            boolean parseSuccess = false;
            try {
                final Snapshot _snapshot = getSnapshot ();
                final FileObject file = _snapshot.getSource().getFileObject();
                if (file != null && !file.isValid()) {
                    return null;
                }
                SourceModificationEvent event = SourceAccessor.getINSTANCE ().getSourceModificationEvent (source);
                TaskProcessor.callParse(_parser, snapshot, task, event);
                SourceAccessor.getINSTANCE ().parsed (source);
                parseSuccess = true;
            } finally {
                if (!parseSuccess) {
                    synchronized (TaskProcessor.INTERNAL_LOCK) {
                        parsed = false;
                    }
                }
            }
        }
        return TaskProcessor.callGetResult(_parser, task);
    }
    
    public void invalidate () {
        synchronized (TaskProcessor.INTERNAL_LOCK) {
            snapshot = null;
            parsed = false;
            embeddings = null;
            providerOrder = null;
            upToDateEmbeddingProviders.clear();
            for (SourceCache sourceCache : embeddingToCache.values ())
                sourceCache.invalidate ();
        }
    }

    public void invalidate (final Snapshot preRendered) {
        synchronized (TaskProcessor.INTERNAL_LOCK) {
            invalidate();
            snapshot = preRendered;
        }
    }
                
    //@GuardedBy(this)
    private volatile List<Embedding>
                            embeddings;

    //@GuardedBy(this)
    private List<EmbeddingProvider>
                            providerOrder;

    //@GuardedBy(this)
    private final Map<EmbeddingProvider,List<Embedding>>
                            embeddingProviderToEmbedings = new HashMap<EmbeddingProvider,List<Embedding>> ();
    
    public Iterable<Embedding> getAllEmbeddings () {
        List<Embedding> result = this.embeddings;
        if (result != null) {
            return result;
        }

retry:  while (true) {
            final Snapshot snpsht = getSnapshot();
            final Collection<SchedulerTask> tsks = createTasks();
            final Set<EmbeddingProvider> currentUpToDateProviders;

            synchronized (TaskProcessor.INTERNAL_LOCK) {
                //BEGIN
                currentUpToDateProviders = new HashSet<EmbeddingProvider>(upToDateEmbeddingProviders);
            }

            final Map<EmbeddingProvider,List<Embedding>> newEmbeddings = new LinkedHashMap<EmbeddingProvider, List<Embedding>> ();
            for (SchedulerTask schedulerTask : tsks) {
                if (schedulerTask instanceof EmbeddingProvider) {
                    final EmbeddingProvider embeddingProvider = (EmbeddingProvider) schedulerTask;
                    if (newEmbeddings.containsKey(embeddingProvider)) {
                        LOG.log(Level.WARNING, "EmbeddingProvider: {0} is registered several time.", embeddingProvider);    //NOI18N
                    }
                    else if (currentUpToDateProviders.contains(embeddingProvider)) {
                        newEmbeddings.put(embeddingProvider,null);
                    } else {
                        final List<Embedding> embForProv = TaskProcessor.callEmbeddingProvider(embeddingProvider,snpsht);
                        if (embForProv == null) {
                            throw new NullPointerException(String.format("The %s returned null embeddings!", embeddingProvider));
                        }
                        newEmbeddings.put(embeddingProvider, embForProv);
                    }
                }
            }

            synchronized (TaskProcessor.INTERNAL_LOCK) {
                if (this.embeddings == null) {
                    if (!upToDateEmbeddingProviders.equals(currentUpToDateProviders)) {
                        //ROLLBACK and RETRY
                        continue retry;
                    }
                    result = new LinkedList<Embedding> ();
                    for (Map.Entry<EmbeddingProvider,List<Embedding>> entry : newEmbeddings.entrySet()) {
                        final EmbeddingProvider embeddingProvider = entry.getKey();
                        final List<Embedding> embeddingsForProvider = entry.getValue();
                        if (embeddingsForProvider == null) {
                            List<Embedding> _embeddings = embeddingProviderToEmbedings.get (embeddingProvider);
                            result.addAll (_embeddings);
                        } else {
                            List<Embedding> oldEmbeddings = embeddingProviderToEmbedings.get (embeddingProvider);
                            updateEmbeddings (embeddingsForProvider, oldEmbeddings, false, null);
                            embeddingProviderToEmbedings.put (embeddingProvider, embeddingsForProvider);
                            upToDateEmbeddingProviders.add (embeddingProvider);
                            result.addAll (embeddingsForProvider);
                        }
                    }
                    //COMMIT
                    this.embeddings = result;
                    this.providerOrder = new ArrayList<EmbeddingProvider>(newEmbeddings.keySet());
                }                
                return this.embeddings;
            }
        }
    }

    //@GuardedBy(this)
    private final Set<EmbeddingProvider> upToDateEmbeddingProviders = new HashSet<EmbeddingProvider> ();

    
    void refresh (EmbeddingProvider embeddingProvider, Class<? extends Scheduler> schedulerType) {
        List<Embedding> _embeddings = TaskProcessor.callEmbeddingProvider(embeddingProvider, getSnapshot ());
        List<Embedding> oldEmbeddings;
        synchronized (TaskProcessor.INTERNAL_LOCK) {
            oldEmbeddings = embeddingProviderToEmbedings.get (embeddingProvider);
            updateEmbeddings (_embeddings, oldEmbeddings, true, schedulerType);
            embeddingProviderToEmbedings.put (embeddingProvider, _embeddings);            
            upToDateEmbeddingProviders.add (embeddingProvider);
            if (this.embeddings != null) {
                final Embedding insertBefore = findNextEmbedding(providerOrder, embeddingProviderToEmbedings, embeddingProvider);
                this.embeddings.removeAll(oldEmbeddings);
                int index = insertBefore == null ? -1 : this.embeddings.indexOf(insertBefore);
                this.embeddings.addAll(index == -1 ? this.embeddings.size() : index, _embeddings);
            }
        }
    }

    private Embedding findNextEmbedding (
            final List<EmbeddingProvider> order,
            final Map<EmbeddingProvider,List<Embedding>> providers2embs,
            final EmbeddingProvider provider) {
        if (order != null) {
            boolean accept = false;
            for (EmbeddingProvider p : order) {
                if (accept) {
                    final Collection<Embedding> c = providers2embs.get(p);
                    if (c != null && !c.isEmpty()) {
                        return c.iterator().next();
                    }
                } else if (p.equals(provider)) {
                    accept = true;
                }
            }
        }
        return null;
    }
    
    private void updateEmbeddings (
            final List<Embedding>                 embeddings,
            final List<Embedding>                 oldEmbeddings,
            final boolean                         updateTasks,
            final Class<? extends Scheduler>      schedulerType) {
        assert Thread.holdsLock(TaskProcessor.INTERNAL_LOCK);
        final List<SourceCache> toBeSchedulled = new ArrayList<SourceCache> ();
            
        if (oldEmbeddings != null && embeddings.size () == oldEmbeddings.size ()) {
            for (int i = 0; i < embeddings.size (); i++) {
                if (embeddings.get (i) == null) {
                    throw new NullPointerException ();
                }
                SourceCache cache = embeddingToCache.remove (oldEmbeddings.get (i));
                if (cache != null) {
                    cache.setEmbedding (embeddings.get (i));
                    assert embeddings.get (i).getMimeType ().equals (cache.getSnapshot ().getMimeType ());
                    embeddingToCache.put (embeddings.get (i), cache);
                } else {
                    cache = getCache(embeddings.get(i));
                }

                if (updateTasks) {
                    toBeSchedulled.add (cache);
                }
            }
        } else {
            if (oldEmbeddings != null) {
                for (Embedding _embedding : oldEmbeddings) {
                    SourceCache cache = embeddingToCache.remove (_embedding);
                    if (cache != null) {
                        cache.removeTasks ();
                    }
                }
            }
            if (updateTasks) {
                for (Embedding _embedding : embeddings) {
                    SourceCache cache = getCache (_embedding);
                    toBeSchedulled.add (cache);
                }
            }
        }        
        for (SourceCache cache : toBeSchedulled) {
            cache.scheduleTasks (schedulerType);
        }
    }
    
    //@GuardedBy(this)
    private final Map<Embedding,SourceCache>
                            embeddingToCache = new HashMap<Embedding,SourceCache> ();
    
    public SourceCache getCache (Embedding embedding) {
        synchronized (TaskProcessor.INTERNAL_LOCK) {
            SourceCache sourceCache = embeddingToCache.get (embedding);
            if (sourceCache == null) {
                sourceCache = new SourceCache (source, embedding);
                assert embedding.getMimeType ().equals (sourceCache.getSnapshot ().getMimeType ());
                embeddingToCache.put (embedding, sourceCache);
            }
            return sourceCache;
        }
    }

    
    // tasks management ........................................................
    
    //@GuardedBy(this)
    private List<SchedulerTask> 
                            tasks;
    //@GuardedBy(this)
    private Set<SchedulerTask> 
                            pendingTasks;
    
    private Collection<SchedulerTask> createTasks () {
        List<SchedulerTask> tasks1 = null;
        Set<SchedulerTask> pendingTasks1 = null;
        if (tasks == null) {
            tasks1 = new ArrayList<SchedulerTask> ();
            pendingTasks1 = new HashSet<SchedulerTask> ();
            Lookup lookup = MimeLookup.getLookup (mimeType);
            Collection<? extends TaskFactory> factories = lookup.lookupAll (TaskFactory.class);
            Snapshot fakeSnapshot = null;
            for (TaskFactory factory : factories) {
                // #185586 - this is here in order not to create snapshots (a copy of file/doc text)
                // if there is no task that would really need it (eg. in C/C++ projects there is no parser
                // registered and no tasks will ever run on these files, even though there may be tasks
                // registered for all mime types
                Collection<? extends SchedulerTask> newTasks = factory.create(getParser() != null ? 
                    getSnapshot() :
                    fakeSnapshot == null ?
                        fakeSnapshot = SourceAccessor.getINSTANCE().createSnapshot(
                            "", //NOI18N
                            new int [] { 0 },
                            source,
                            MimePath.get (mimeType),
                            new int[][] {new int[] {0, 0}},
                            new int[][] {new int[] {0, 0}}) :
                        fakeSnapshot
                );
                if (newTasks != null) {
                    tasks1.addAll (newTasks);
                    pendingTasks1.addAll (newTasks);
                }
            }
            Collections.sort(tasks1, PRIORITY_ORDER);
        }
        synchronized (TaskProcessor.INTERNAL_LOCK) {
            if ((tasks == null) && (tasks1 != null)) {
                tasks = tasks1;
                pendingTasks = pendingTasks1;
            }
            if (tasks != null) {
                // this should be normal return in most cases
                return tasks;
            }
        }
        // recurse and hope
        return createTasks();
    }
    
    //tzezula: probably has race condition
    public void scheduleTasks (Class<? extends Scheduler> schedulerType) {
        //S ystem.out.println("scheduleTasks " + schedulerType);
        final List<SchedulerTask> remove = new ArrayList<SchedulerTask> ();
        final List<Pair<SchedulerTask,Class<? extends Scheduler>>> add = new ArrayList<Pair<SchedulerTask,Class<? extends Scheduler>>> ();
        Collection<SchedulerTask> tsks = createTasks();
        synchronized (TaskProcessor.INTERNAL_LOCK) {
            for (SchedulerTask task : tsks) {
                if (schedulerType == null || task instanceof EmbeddingProvider) {
                    if (!pendingTasks.remove (task)) {
                        remove.add (task);
                    }                   
                    add.add (Pair.<SchedulerTask,Class<? extends Scheduler>>of(task,null));
                } else if (task.getSchedulerClass () == schedulerType) {
                    if (!pendingTasks.remove (task)) {
                        remove.add (task);
                    }
                    add.add (Pair.<SchedulerTask,Class<? extends Scheduler>>of(task,schedulerType));
                }
            }
        }        
        if (!add.isEmpty ()) {
            LOG.fine("Change tasks for source " + source + " - add: " + add + ", remove " + remove);
            TaskProcessor.updatePhaseCompletionTask(add, remove, source, this);
        }
    }

    public void unscheduleTasks (Class<? extends Scheduler> schedulerType) {
        //S ystem.out.println("unscheduleTasks " + schedulerType);
        final List<SchedulerTask> remove = new ArrayList<SchedulerTask> ();
        synchronized (TaskProcessor.INTERNAL_LOCK) {
            if (tasks != null) {
                for (SchedulerTask task : tasks) {
                    if (schedulerType == null ||
                        task.getSchedulerClass () == schedulerType ||
                        task instanceof EmbeddingProvider
                    ) {
                        remove.add (task);
                    }
                }
            }
        }
        if (!remove.isEmpty ()) {
            TaskProcessor.removePhaseCompletionTasks(remove, source);
        }
    }

    //jjancura: probably has race condition too
    public void sourceModified () {
        SourceModificationEvent sourceModificationEvent = SourceAccessor.getINSTANCE ().getSourceModificationEvent (source);
        if (sourceModificationEvent == null)
            return;
        final Map<Class<? extends Scheduler>,? extends SchedulerEvent> schedulerEvents =
                SourceAccessor.getINSTANCE ().createSchedulerEvents (source, 
                        Utilities.getEnvFactory().getSchedulers(source.getLookup()), sourceModificationEvent);
        if (schedulerEvents.isEmpty ()) {
            return;
        }
        final List<SchedulerTask> remove = new ArrayList<SchedulerTask> ();
        final List<Pair<SchedulerTask,Class<? extends Scheduler>>> add = new ArrayList<Pair<SchedulerTask,Class<? extends Scheduler>>>();
        Collection<SchedulerTask> tsks = createTasks();
        synchronized (TaskProcessor.INTERNAL_LOCK) {
            for (SchedulerTask task : tsks) {
                Class<? extends Scheduler> schedulerClass = task.getSchedulerClass ();
                if (schedulerClass != null &&
                    !schedulerEvents.containsKey (schedulerClass)
                )
                    continue;
                if (!pendingTasks.remove (task)) {
                    remove.add (task);
                }
                add.add (Pair.<SchedulerTask,Class<? extends Scheduler>>of(task,null));
            }
        }
        if (!add.isEmpty ()) {
            TaskProcessor.updatePhaseCompletionTask (add, remove, source, this);
        }
    }
    
    @Override
    public String toString () {
        StringBuilder sb = new StringBuilder ("SourceCache ");
        sb.append (hashCode ());
        sb.append (": ");
        Snapshot _snapshot = getSnapshot ();
        Source _source = _snapshot.getSource ();
        FileObject fileObject = _source.getFileObject ();
        if (fileObject != null)
            sb.append (fileObject.getNameExt ());
        else
            sb.append (mimeType).append (" ").append (_source.getDocument (false));
        if (!_snapshot.getMimeType ().equals (_source.getMimeType ())) {
            sb.append ("( ").append (_snapshot.getMimeType ()).append (" ");
            sb.append (_snapshot.getOriginalOffset (0)).append ("-").append (_snapshot.getOriginalOffset (_snapshot.getText ().length () - 1)).append (")");
        }
        return sb.toString ();
    }
    
    //@NotThreadSafe - has to be called in GuardedBy(this)
    private void removeTasks () {
        if (tasks != null) {
            TaskProcessor.removePhaseCompletionTasks (tasks, source);
        }
        tasks = null;
    }
}




