/*
 * 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.apache.sling.installer.provider.jcr.impl;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.jcr.Item;
import javax.jcr.Node;
import javax.jcr.NodeIterator;
import javax.jcr.RepositoryException;
import javax.jcr.Session;

import org.apache.sling.installer.api.InstallableResource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/** Watch a single folder in the JCR Repository, detecting changes
 *  to it and providing InstallableData for its contents.
 */
class WatchedFolder {

    private final Logger logger = LoggerFactory.getLogger(getClass());

    private final String path;
    private final String pathWithSlash;
    private final int priority;
    private final Session session;
    private final Collection <JcrInstaller.NodeConverter> converters;
    private final Set<String> existingResourceUrls = new HashSet<String>();

    private volatile boolean needsScan;

    static class ScanResult {
        List<InstallableResource> toAdd = new ArrayList<InstallableResource>();
        List<String> toRemove = new ArrayList<String>();
    };

    /** Store the digests of the last returned resources, keyed by path, to detect changes */
    private final Map<String, String> digests = new HashMap<String, String>();

    WatchedFolder(final Session session,
            final String path,
            final int priority,
    		final Collection<JcrInstaller.NodeConverter> converters)
    throws RepositoryException {
        if (priority < 1) {
            throw new IllegalArgumentException("Cannot watch folder with priority 0:" + path);
        }

        this.path = path;
        this.pathWithSlash = path.concat("/");
        this.converters = converters;
        this.priority = priority;
        this.session = session;
    }

    public void start() {
        logger.info("Watching folder {} (priority {})", path, priority);
        this.needsScan = true;
    }

    @Override
    public String toString() {
    	return getClass().getSimpleName() + ":" + path;
    }

    public String getPath() {
        return path;
    }

    public String getPathWithSlash() {
        return this.pathWithSlash;
    }

    /**
     * Update scan flag whenever an observation event occurs.
     */
    public void markForScan() {
        logger.debug("JCR events received for path {}", path);
        needsScan = true;
    }

    /**
     * Did an observation event occur in the meantime?
     */
    public boolean needsScan() {
    	return needsScan;
    }

    /**
     * Scan the contents of our folder and return the corresponding
     * <code>ScanResult</code> containing the <code>InstallableResource</code>s.
     */
    public ScanResult scan() throws RepositoryException {
        logger.debug("Scanning {}", path);
        needsScan = false;

        Node folder = null;
        if (session.itemExists(path)) {
        	final Item i = session.getItem(path);
        	if(i.isNode()) {
        		folder = (Node)i;
        	}
        }

        // Return an InstallableResource for all child nodes for which we have a NodeConverter
        final ScanResult result = new ScanResult();
        final Set<String> resourcesSeen = new HashSet<String>();
        if (folder != null) {
            scanNode(folder, result, resourcesSeen);
        }

        // Resources that existed but are not in resourcesSeen need to be
        // unregistered from OsgiInstaller
        for(final String url : existingResourceUrls) {
        	if(!resourcesSeen.contains(url)) {
                result.toRemove.add(url);
        	}
        }
        for(final String u : result.toRemove) {
        	existingResourceUrls.remove(u);
        	digests.remove(u);
        }

        // Update saved digests of the resources that we're returning
        for(final InstallableResource r : result.toAdd) {
            existingResourceUrls.add(r.getId());
            digests.put(r.getId(), r.getDigest());
        }

        return result;
    }

    private void scanNode(final Node folder, final ScanResult result, final Set<String> resourcesSeen)
    throws RepositoryException {
        final NodeIterator it = folder.getNodes();
        while(it.hasNext()) {
            final Node n = it.nextNode();
            boolean processed = false;
            for (JcrInstaller.NodeConverter nc : converters) {
                final InstallableResource r = nc.convertNode(n, priority);
                if(r != null) {
                    processed = true;
                    resourcesSeen.add(r.getId());
                    final String oldDigest = digests.get(r.getId());
                    if (r.getDigest().equals(oldDigest)) {
                        logger.debug("Digest didn't change, ignoring " + r);
                    } else {
                        result.toAdd.add(r);
                    }
                    break;
                }
            }
            if ( !processed ) {
                this.scanNode(n, result, resourcesSeen);
            }
        }
    }
}

@@ -3699,35 +3699,8 @@
 th);
-%0A        needsScan = false;
 %0A%0A  
@@ -4832,32 +4832,60 @@
 ());%0A        %7D%0A%0A
+        needsScan = false;%0A%0A
         return r

Set needsScan to false at the end of a scan instead of at the beginning in order to keep scanning when an exception occurs during a scan (SLING-6786).