/*
 * Copyright 2016-2022 chronicle.software
 *
 *       https://chronicle.software
 *
 * 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 net.openhft.chronicle.queue.impl.single;

import net.openhft.chronicle.core.Jvm;
import net.openhft.chronicle.core.io.*;
import net.openhft.chronicle.core.util.ThrowingFunction;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;

/**
 * Thread-safe, self-cleaning cache for ReferenceCounted (and Closeable) objects
 */
public class ReferenceCountedCache<K, T extends ReferenceCounted & Closeable, V, E extends Throwable>
        extends AbstractCloseable {

    private final Map<K, T> cache = new LinkedHashMap<>();
    private final Function<T, V> transformer;
    private final ThrowingFunction<K, T, E> creator;
    private final Runnable bgCleanup = this::bgCleanup;

    public ReferenceCountedCache(final Function<T, V> transformer,
                                 final ThrowingFunction<K, T, E> creator) {
        this.transformer = transformer;
        this.creator = creator;

        singleThreadedCheckDisabled(true);
    }

    @NotNull
    V get(@NotNull final K key) throws E {
        throwExceptionIfClosed();

        final V rv;
        synchronized (cache) {
            @Nullable T value = cache.get(key);

            if (value == null) {
                value = creator.apply(key);
                value.reserveTransfer(INIT, this);
                //System.err.println("Reserved " + value.toString() + " by " + this);
                cache.put(key, value);
            }

            // this will add to the ref count and so needs to be done inside of sync block
            rv = transformer.apply(value);
        }

        BackgroundResourceReleaser.run(bgCleanup);

        return rv;
    }

    @Override
    protected void performClose() {
        List<T> retained = new ArrayList<>();
        synchronized (cache) {
            for (T value : cache.values()) {
                try {
                    value.release(this);
                    if (value.refCount() > 0)
                        retained.add(value);
//                    System.err.println("Released (performClose) " + value + " by " + this);
                } catch (Exception e) {
                    Jvm.debug().on(getClass(), e);
                }
            }
            cache.clear();
        }
        if (retained.isEmpty() || !Jvm.isResourceTracing())
            return;
        for (int i = 1; i <= 2_500; i++) {
            Jvm.pause(1);
            if (retained.stream().noneMatch(v -> v.refCount() > 0)) {
                (i > 9 ? Jvm.perf() : Jvm.debug())
                        .on(getClass(), "Took " + i + " ms to release " + retained);
                return;
            }
        }
        retained.stream()
                .filter(o -> o instanceof ManagedCloseable)
                .map(o -> (ManagedCloseable) o)
                .forEach(ManagedCloseable::warnAndCloseIfNotClosed);
    }

    void bgCleanup() {
        // remove all which have been de-referenced by other than me. Garbagy but rare
        synchronized (cache) {
            cache.entrySet().removeIf(entry -> {
                T value = entry.getValue();
                int refCount = value.refCount();
                if (refCount == 1) {
                    value.release(this);
                }
                return refCount <= 1;
            });
        }
    }
}

@@ -3111,77 +3111,279 @@
 rn;%0A
-        for (int i = 1; i %3C= 2_500; i++) %7B%0A            Jvm.pause(1);%0A
+%0A        boolean interrupted = false;%0A        try %7B%0A            for (int i = 1; i %3C= 2_500; i++) %7B%0A                try %7B%0A                    Thread.sleep(1);%0A                %7D catch (InterruptedException e) %7B%0A                    interrupted = true;%0A                %7D%0A    
     
@@ -3464,16 +3464,20 @@
         
+    
 (i %3E 9 ?
@@ -3527,16 +3527,20 @@
         
+ 
+   
 .on(getC
@@ -3608,24 +3608,28 @@
         
+   
+ 
 return;%0A
         
@@ -3620,16 +3620,20 @@
 return;%0A
+    
         
@@ -3646,26 +3646,32 @@
         
-%7D%0A
+    %7D%0A  
         
 retained
@@ -3654,32 +3654,34 @@
     %7D%0A          
+  
 retained.stream(
@@ -3682,16 +3682,20 @@
 tream()%0A
+    
         
@@ -3762,16 +3762,20 @@
         
+    
 .map(o -
@@ -3814,16 +3814,20 @@
         
+  
+  
 .forEach
@@ -3867,24 +3867,151 @@
 NotClosed);%0A
+        %7D finally %7B%0A            if (interrupted) %7B%0A                Thread.currentThread().interrupt();%0A            %7D%0A        %7D%0A
     %7D%0A%0A    v

Improve handling of close() when interrupted