/*
 * Copyright 2017 The RoboZonky Project
 *
 * 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 com.github.robozonky.app.util;

import java.time.Duration;
import java.time.Instant;
import java.util.Optional;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Supplier;

import com.github.robozonky.api.remote.entities.sanitized.Investment;
import com.github.robozonky.api.remote.entities.sanitized.Loan;
import com.github.robozonky.common.remote.Zonky;
import com.github.robozonky.util.Scheduler;
import org.eclipse.collections.api.map.primitive.MutableIntObjectMap;
import org.eclipse.collections.api.tuple.Pair;
import org.eclipse.collections.impl.map.mutable.primitive.IntObjectHashMap;
import org.eclipse.collections.impl.tuple.Tuples;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class LoanCache {

    public static final LoanCache INSTANCE = new LoanCache();
    private static final int INITIAL_CACHE_SIZE = 20;
    private static final Logger LOGGER = LoggerFactory.getLogger(LoanCache.class);
    private static final Duration EVICT_AFTER = Duration.ofHours(1);
    private final Lock updateLock = new ReentrantLock(true);
    private volatile MutableIntObjectMap<Pair<Loan, Instant>> cache;

    LoanCache() {
        clean();
        Scheduler.inBackground().submit(this::evict, Duration.ofMinutes(30)); // schedule automated eviction
    }

    private static boolean isExpired(final Pair<Loan, Instant> p) {
        final Instant deadline = Instant.now().minus(EVICT_AFTER);
        return p.getTwo().isBefore(deadline);
    }

    private void evict() {
        LOGGER.trace("Evicting loans.");
        runLocked(() -> cache = cache.reject((value, p) -> isExpired(p)));
        LOGGER.trace("Evicted.");
    }

    private void runLocked(final Runnable runnable) {
        updateLock.lock();
        try {
            runnable.run();
        } finally {
            updateLock.unlock();
        }
    }

    private <T> T callLocked(final Supplier<T> runnable) {
        updateLock.lock();
        try {
            return runnable.get();
        } finally {
            updateLock.unlock();
        }
    }

    public Optional<Loan> getLoan(final int loanId) {
        return Optional.ofNullable(callLocked(() -> {
            final Pair<Loan, Instant> result = cache.get(loanId);
            if (result == null) {
                LOGGER.trace("Cache miss for loan #{}.", loanId);
                return null;
            } else if (isExpired(result)) {
                LOGGER.trace("Evicting expired loan #{}.", loanId);
                cache.remove(loanId);
                return null;
            } else {
                return result.getOne();
            }
        }));
    }

    private void addLoan(final int loanId, final Loan loan) {
        runLocked(() -> cache.put(loanId, Tuples.pair(loan, Instant.now())));
    }

    public Loan getLoan(final int loanId, final Zonky api) {
        return getLoan(loanId).orElseGet(() -> {
            final Loan l = api.getLoan(loanId);
            addLoan(loanId, l);
            return l;
        });
    }

    public Loan getLoan(final Investment investment, final Zonky api) {
        final Loan loan = getLoan(investment.getLoanId(), api);
        /*
         * investment may have been created without access to the loan. now that we have the loan, we can update the
         * investment, filling any missing information.
         */
        Investment.fillFrom(investment, loan);
        return loan;
    }

    public void clean() {
        runLocked(() -> cache = new IntObjectHashMap<>(INITIAL_CACHE_SIZE));
    }
}

@@ -1523,31 +1523,24 @@
     private 
-static 
 final Logger
@@ -1591,16 +1591,16 @@
 class);%0A
+
     priv
@@ -1822,24 +1822,87 @@
    clean();%0A
+        final Duration thirtyMinutes = Duration.ofMinutes(30);%0A
         Sche
@@ -1942,38 +1942,44 @@
 :evict, 
-Duration.of
+thirtyMinutes, thirty
 Minutes
-(30)
 ); // sc
@@ -2839,66 +2839,8 @@
 ) %7B%0A
-        return Optional.ofNullable(callLocked(() -%3E %7B%0A    
     
@@ -2877,16 +2877,33 @@
 result =
+ callLocked(() -%3E
  cache.g
@@ -2904,38 +2904,35 @@
 ache.get(loanId)
+)
 ;%0A
-    
         if (resu
@@ -2945,130 +2945,12 @@
 null
-) %7B%0A                LOGGER.trace(%22Cache miss for loan #%7B%7D.%22, loanId);%0A                return null;%0A            %7D else if (
+ %7C%7C 
 isEx
@@ -2974,28 +2974,24 @@
             
- 
-   
 LOGGER.trace
@@ -2996,24 +2996,22 @@
 ce(%22
-Evicting expired
+Cache miss for
  loa
@@ -3044,67 +3044,33 @@
     
-    cache.remove(loanId);%0A                return null;%0A    
+return Optional.empty();%0A
     
@@ -3094,27 +3094,35 @@
         
-    
 return 
+Optional.of(
 result.g
@@ -3128,16 +3128,17 @@
 getOne()
+)
 ;%0A      
@@ -3143,26 +3143,9 @@
     
-  
-  %7D%0A        %7D));
+%7D
 %0A   

Use simpler cache retrieval, reducing need for locking