/*
 * 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.felix.fileinstall.internal;

import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.zip.CRC32;

/**
 * A Scanner object is able to detect and report new, modified
 * and deleted files.
 *
 * The scanner use an internal checksum to identify the signature
 * of a file or directory.  The checksum will change if the file
 * or any of the directory's child is modified.
 *
 * In addition, if the scanner detects a change on a given file, it
 * will wait until the checksum does not change anymore before reporting
 * the change on this file.  This allows to not report the change until
 * a big copy if complete for example.
 */
public class Scanner {

    final File directory;
    final FilenameFilter filter;

    // Store checksums of files or directories
    Map/* <File, Long> */ lastChecksums = new HashMap/* <File, Long> */();
    Map/* <File, Long> */ storedChecksums = new HashMap/* <File, Long> */();

    /**
     * Create a scanner for the specified directory
     *
     * @param directory the directory to scan
     */
    public Scanner(File directory)
    {
        this(directory, null);
    }

    /**
     * Create a scanner for the specified directory and file filter
     *
     * @param directory the directory to scan
     * @param filter a filter for file names
     */
    public Scanner(File directory, FilenameFilter filter)
    {
        this.directory = directory;
        this.filter = filter;
    }

    /**
     * Initialize the list of known files.
     * This should be called before the first scan to initialize
     * the list of known files.  The purpose is to be able to detect
     * files that have been deleted while the scanner was inactive.
     *
     * @param checksums a map of checksums
     */
    public void initialize(Map/*<File, Long>*/ checksums)
    {
        storedChecksums.putAll(checksums);
    }

    /**
     * Report a set of new, modified or deleted files.
     * Modifications are checked against a computed checksum on some file
     * attributes to detect any modification.
     * Upon restart, such checksums are not known so that all files will
     * be reported as modified. 
     *
     * @param reportImmediately report all files immediately without waiting for the checksum to be stable
     * @return a list of changes on the files included in the directory
     */
    public Set/*<File>*/ scan(boolean reportImmediately)
    {
        File[] list = directory.listFiles(filter);
        if (list == null)
        {
            return null;
        }
        Set/*<File>*/ files = new TreeSet/*<File>*/(new FileModificationTimeComparator());
        Set/*<File>*/ removed = new HashSet/*<File>*/(storedChecksums.keySet());
        for (int i = 0; i < list.length; i++)
        {
            File file  = canon(list[i]);
            long lastChecksum = lastChecksums.get(file) != null ? ((Long) lastChecksums.get(file)).longValue() : 0;
            long storedChecksum = storedChecksums.get(file) != null ? ((Long) storedChecksums.get(file)).longValue() : 0;
            long newChecksum = checksum(file);
            lastChecksums.put(file, new Long(newChecksum));
            // Only handle file when it does not change anymore and it has changed since last reported
            if ((newChecksum == lastChecksum || reportImmediately) && newChecksum != storedChecksum)
            {
                storedChecksums.put(file, new Long(newChecksum));
                files.add(file);
            }
            removed.remove(file);
        }
        for (Iterator it = removed.iterator(); it.hasNext();)
        {
            File file = (File) it.next();
            // Make sure we'll handle a file that has been deleted
            files.addAll(removed);
            // Remove no longer used checksums
            lastChecksums.remove(file);
            storedChecksums.remove(file);
        }
        return files;
    }

    private static File canon(File file)
    {
        try
        {
            return file.getCanonicalFile();
        }
        catch (IOException e)
        {
            return file;
        }
    }

    /**
     * Retrieve the previously computed checksum for a give file.
     *
     * @param file the file to retrieve the checksum
     * @return the checksum
     */
    public long getChecksum(File file)
    {
        Long c = (Long) storedChecksums.get(file);
        return c != null ? c.longValue() : 0;
    }

    /**
     * Compute a cheksum for the file or directory that consists of the name, length and the last modified date
     * for a file and its children in case of a directory
     *
     * @param file the file or directory
     * @return a checksum identifying any change
     */
    static long checksum(File file)
    {
        CRC32 crc = new CRC32();
        checksum(file, crc);
        return crc.getValue();
    }

    private static void checksum(File file, CRC32 crc)
    {
        crc.update(file.getName().getBytes());
        if (file.isFile())
        {
            checksum(file.lastModified(), crc);
            checksum(file.length(), crc);
        }
        else if (file.isDirectory())
        {
            File[] children = file.listFiles();
            if (children != null)
            {
                for (int i = 0; i < children.length; i++)
                {
                    checksum(children[i], crc);
                }
            }
        }
    }

    private static void checksum(long l, CRC32 crc)
    {
        for (int i = 0; i < 8; i++)
        {
            crc.update((int) (l & 0x000000ff));
            l >>= 8;
        }
    }

    /**
     * {@link Comparator} that sorts {@link File}s in increasing order of modification time
     * ("oldest first").
     */
    private final static class FileModificationTimeComparator implements Comparator
    {
        public int compare(Object arg0, Object arg1)
        {
            File lhs = (File) arg0;
            File rhs = (File) arg1;
            return (int) (lhs.lastModified() - rhs.lastModified());
        }
    }

}

@@ -1085,34 +1085,8 @@
 et;%0A
-import java.util.TreeSet;%0A
 impo
@@ -1109,16 +1109,16 @@
 .CRC32;%0A
+
 %0A/**%0A * 
@@ -3574,12 +3574,12 @@
 new 
-Tree
+Hash
 Set/
@@ -3592,44 +3592,8 @@
 %3E*/(
-new FileModificationTimeComparator()
 );%0A 
@@ -6531,24 +6531,24 @@
 000000ff));%0A
+
             
@@ -6577,449 +6577,6 @@
  %7D%0A%0A
-    /**%0A     * %7B@link Comparator%7D that sorts %7B@link File%7Ds in increasing order of modification time%0A     * (%22oldest first%22).%0A     */%0A    private final static class FileModificationTimeComparator implements Comparator%0A    %7B%0A        public int compare(Object arg0, Object arg1)%0A        %7B%0A            File lhs = (File) arg0;%0A            File rhs = (File) arg1;%0A            return (int) (lhs.lastModified() - rhs.lastModified());%0A        %7D%0A    %7D%0A%0A
 %7D%0A

Revert FELIX-2201 which cause regressions