content: /*
   *  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.causeway.testdomain.util.kv;
  
  import java.util.Map;
  import java.util.Optional;
  import java.util.concurrent.CountDownLatch;
  
  import jakarta.annotation.PostConstruct;
  import jakarta.annotation.PreDestroy;
  
  import org.jspecify.annotations.NonNull;
  
  import org.springframework.stereotype.Service;
  
  import org.apache.causeway.commons.collections.Can;
  import org.apache.causeway.commons.internal.collections._Maps;
  
  import lombok.SneakyThrows;
  import lombok.extern.log4j.Log4j2;
  
  @Service
  @Log4j2
  public class KVStoreForTesting {
  
      private Map<Key, Object> keyValueMap;
      private Map<Class<?>, CountDownLatch> latchMap;
  
      @PostConstruct
      public void init() {
          log.info("about to initialize");
          keyValueMap = _Maps.newConcurrentHashMap();
          latchMap = _Maps.newConcurrentHashMap();
      }
  
      @PreDestroy
      public void shutdown() {
          log.info("about to shutdown");
          keyValueMap.clear();
      }
  
      public void clearValues() {
          keyValueMap.clear();
      }
  
      public void put(final Object caller, final String keyStr, final Object value) {
          var key = new Key(caller.getClass(), keyStr);
          log.debug("writing {} -> {}", key, value);
          keyValueMap.put(key, value);
          var latch = latchMap.remove(caller.getClass());
          if(latch!=null) {
              latch.countDown();
          }
      }
  
      public Optional<Object> get(final Class<?> callerType, final String keyStr) {
          return Optional.ofNullable(keyValueMap.get(new Key(callerType, keyStr)));
      }
  
      public Optional<Object> get(final Object caller, final String keyStr) {
          return get(caller.getClass(), keyStr);
      }
  
      // -- NON-SCALAR SUPPORT
  
      public void append(final Object caller, final String keyStr, final Object value) {
          var canRef = get(caller, keyStr);
          if(! canRef.isPresent()) {
              put(caller, keyStr, Can.ofSingleton(value));
          } else {
              @SuppressWarnings("unchecked")
              var newCan = ((Can<Object>)canRef.get()).add(value);
              put(caller, keyStr, newCan);
          }
      }
  
      @SuppressWarnings("unchecked")
      public Can<Object> getAll(final Class<?> callerType, final String keyStr) {
          var canRef = get(callerType, keyStr);
          return ! canRef.isPresent()
              ? Can.empty()
              : ((Can<Object>)canRef.get());
      }
  
      public Can<Object> getAll(final Object caller, final String keyStr) {
          return getAll(caller.getClass(), keyStr);
      }
  
      // -- COUNTING
  
      public long incrementCounter(final Class<?> callerType, final String keyStr) {
          var key = new Key(callerType, keyStr);
          return (long) keyValueMap.compute(key, (k, v) -> (v == null) ? 1L : 1L + (long)v);
      }
  
      public long getCounter(final Class<?> callerType, final String keyStr) {
          var key = new Key(callerType, keyStr);
          return (long) keyValueMap.getOrDefault(key, 0L);
      }
  
      // --
  
      public void clear(final Class<?> callerType) {
          log.debug("clearing {}", callerType);
          keyValueMap.entrySet()
          .removeIf(entry->entry.getKey().caller().equals(callerType));
      }
  
      public void clear(final Object caller) {
          clear(caller.getClass());
      }
  
      public long countEntries(final Class<?> callerType) {
          return keyValueMap.entrySet()
              .stream()
              .filter(entry->entry.getKey().caller().equals(callerType))
              .count();
      }
  
      private record Key(
              @NonNull Class<?> caller,
              @NonNull String keyStr) {
      }
  
      /** blocks until a new lock becomes available */
      @SneakyThrows
      public void requestLock(final Class<?> callerType) {
          synchronized(this) {
              var latch = latchMap.get(callerType);
              if(latch!=null) {
                  log.warn("entering WAIT state until lock becomes available");
                  latch.await();
                  log.info("lock has become available");
              }
              var newLatch = new CountDownLatch(1);
              latchMap.put(callerType, newLatch);
          }
      }
  
      /** unblocks any threads waiting for a new lock */
      public void releaseLock(final Class<?> callerType) {
          synchronized(this) {
              var latch = latchMap.remove(callerType);
              if(latch!=null) {
                  latch.countDown();
              }
          }
      }
  
  }
  