/*
 * Copyright 2020 the original author or authors.
 *
 * Licensed 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.gradle.api.internal.changedetection.state;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;
import com.google.common.io.ByteStreams;
import org.apache.commons.io.FileUtils;
import org.gradle.api.internal.file.pattern.PathMatcher;
import org.gradle.api.internal.file.pattern.PatternMatcherFactory;
import org.gradle.internal.hash.HashCode;
import org.gradle.internal.hash.Hasher;
import org.gradle.internal.hash.Hashing;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.Nullable;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.function.Supplier;
import java.util.stream.Collectors;

public class PropertiesFileAwareClasspathResourceHasher implements ResourceHasher {
    private static final Logger LOGGER = LoggerFactory.getLogger(PropertiesFileAwareClasspathResourceHasher.class);
    private final ResourceHasher delegate;
    private final Map<PathMatcher, ResourceEntryFilter> propertiesFileFilters;
    private final List<String> propertiesFilePatterns;

    public PropertiesFileAwareClasspathResourceHasher(ResourceHasher delegate, Map<String, ResourceEntryFilter> propertiesFileFilters) {
        this.delegate = delegate;
        ImmutableList.Builder<String> patterns = ImmutableList.builder();
        ImmutableMap.Builder<PathMatcher, ResourceEntryFilter> filters = ImmutableMap.builder();
        propertiesFileFilters.forEach((pattern, resourceEntryFilter) -> {
            filters.put(PatternMatcherFactory.compile(false, pattern), resourceEntryFilter);
            patterns.add(pattern);
        });
        this.propertiesFileFilters = filters.build();
        this.propertiesFilePatterns = patterns.build();
    }

    @Override
    public void appendConfigurationToHasher(Hasher hasher) {
        propertiesFilePatterns.forEach(hasher::putString);
        propertiesFileFilters.values().forEach(resourceEntryFilter -> resourceEntryFilter.appendConfigurationToHasher(hasher));
    }

    @Nullable
    @Override
    public HashCode hash(RegularFileSnapshotContext snapshotContext) {
        ResourceEntryFilter resourceEntryFilter = matchingFiltersFor(snapshotContext.getRelativePathSegments());
        if (resourceEntryFilter == null) {
            return delegate.hash(snapshotContext);
        } else {
            try {
                return hashProperties(FileUtils.readFileToByteArray(new File(snapshotContext.getSnapshot().getAbsolutePath())), resourceEntryFilter);
            } catch (IOException e) {
                LOGGER.debug("Could not load fingerprint for " + snapshotContext.getSnapshot().getAbsolutePath() + ". Falling back to full entry fingerprinting", e);
                return delegate.hash(snapshotContext);
            }
        }
    }

    @Nullable
    @Override
    public HashCode hash(ZipEntryContext zipEntryContext) throws IOException {
        ResourceEntryFilter resourceEntryFilter = matchingFiltersFor(zipEntryContext.getRelativePathSegments());
        if (resourceEntryFilter == null) {
            return delegate.hash(zipEntryContext);
        } else {
            try {
                return hashProperties(ByteStreams.toByteArray(zipEntryContext.getEntry().getInputStream()), resourceEntryFilter);
            } catch (IOException e) {
                LOGGER.debug("Could not load fingerprint for " + zipEntryContext.getRootParentName() + "!" + zipEntryContext.getFullName() + ". Falling back to full entry fingerprinting", e);
                return delegate.hash(zipEntryContext);
            }
        }
    }

    @Nullable
    private ResourceEntryFilter matchingFiltersFor(Supplier<String[]> relativePathSegments) {
        List<ResourceEntryFilter> matchingFilters = propertiesFileFilters.entrySet().stream()
            .filter(entry -> entry.getKey().matches(relativePathSegments.get(), 0))
            .map(Map.Entry::getValue)
            .collect(Collectors.toList());

        if (matchingFilters.size() == 0) {
            return null;
        } else if (matchingFilters.size() == 1) {
            return matchingFilters.get(0);
        } else {
            return new UnionResourceEntryFilter(matchingFilters);
        }
    }

    private HashCode hashProperties(byte[] entryBytes, ResourceEntryFilter propertyResourceFilter) throws IOException {
        Hasher hasher = Hashing.newHasher();
        Properties properties = new Properties();
        properties.load(new InputStreamReader(new ByteArrayInputStream(entryBytes), new PropertyResourceBundleFallbackCharset()));
        Map<String, String> entries = Maps.fromProperties(properties);
        entries
            .entrySet()
            .stream()
            .filter(entry ->
                !propertyResourceFilter.shouldBeIgnored(entry.getKey()))
            .sorted(Map.Entry.comparingByKey())
            .forEach(entry -> {
                hasher.putString(entry.getKey());
                hasher.putString(entry.getValue());
            });
        return hasher.hash();
    }
}

@@ -804,89 +804,8 @@
 ps;%0A
-import com.google.common.io.ByteStreams;%0Aimport org.apache.commons.io.FileUtils;%0A
 impo
@@ -1154,25 +1154,20 @@
 java.io.
-ByteArray
+File
 InputStr
@@ -1186,20 +1186,27 @@
 java.io.
-File
+IOException
 ;%0Aimport
@@ -1211,34 +1211,34 @@
 rt java.io.I
-OException
+nputStream
 ;%0Aimport jav
@@ -3071,38 +3071,26 @@
 try 
-%7B%0A                return hashP
+(FileInputStream p
 rope
@@ -3098,47 +3098,45 @@
 ties
-(
 File
-Utils.readFileToByteArray(new File
+InputStream = new FileInputStream
 (sna
@@ -3181,16 +3181,81 @@
 Path()))
+%7B%0A                return hashProperties(propertiesFileInputStream
 , resour
@@ -3951,32 +3951,8 @@
 ies(
-ByteStreams.toByteArray(
 zipE
@@ -3990,17 +3990,16 @@
 Stream()
-)
 , resour
@@ -4999,25 +4999,31 @@
 ies(
-byte%5B%5D entryBytes
+InputStream inputStream
 , Re
@@ -5230,44 +5230,19 @@
 der(
-new ByteArrayInputStream(entryBytes)
+inputStream
 , ne

Remove unnecessary transition to bytes for properties files, too