<commit_before>package dev.kkorolyov.pancake.sound;

import dev.kkorolyov.pancake.Config;
import dev.kkorolyov.simplelogs.Logger;
import dev.kkorolyov.simpleprops.Properties;

import javafx.scene.media.AudioClip;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.NoSuchElementException;
import java.util.Random;

/**
 * Provides access to sounds by name.
 * Names are mapped to an arbitrary-length collection of sound resources.
 * When retrieving from a name with multiple resource associations, a randomly-selected associated resource is returned.
 */
public class SoundPool {
	private static final int DEFAULT_CACHE_SIZE = 15;
	private static final Random rand = new Random();
	private static final Logger log = Config.getLogger(SoundPool.class);

	private final Map<String, List<String>> resourceLists = new HashMap<>();
	private final Map<String, AudioClip> cache;
	private int cacheSize;

	/**
	 * Constructs a new sound pool using the {@link #DEFAULT_CACHE_SIZE}.
	 */
	public SoundPool() {
		this(DEFAULT_CACHE_SIZE);
	}
	/**
	 * Constructs a new sound pool using some cache size.
	 * @param cacheSize maximum size of audio file cache
	 */
	public SoundPool(int cacheSize) {
		cache = new LinkedHashMap<String, AudioClip>(cacheSize) {
			@Override
			protected boolean removeEldestEntry(Entry eldest) {
				return size() > getCacheSize();
			}
		};
		setCacheSize(cacheSize);

		log.debug("Constructed a new SoundPool with cacheSize={}", getCacheSize());
	}

	/**
	 * Retrieves a sound randomly selected from all resources associated with a name.
	 * @param name sound name
	 * @return randomly-selected sound associated with {@code name}
	 * @throws NoSuchElementException if no sounds associated with {@code name}
	 */
	public AudioClip get(String name) {
		return cache.computeIfAbsent(getResource(name), AudioClip::new);
	}
	private String getResource(String name) {
		List<String> resources = resourceLists.get(name);
		if (resources == null) throw new NoSuchElementException("No resources associated with name: " + name);

		return resources.get(rand.nextInt(resources.size()));
	}

	/**
	 * Parses sounds from a configuration file.
	 * Each entry is a sound name mapped to an arbitrary-length list of resources.
	 * If a resource does not contain {@code //}, it is assumed to be found on the local filesystem.
	 * @param soundConfig sound configuration file
	 */
	public void put(Properties soundConfig) {
		for (String name : soundConfig.keys()) {
			for (String resource : soundConfig.getArray(name)) {
				String url = resource;

				if (!resource.contains("//")) {
					URL systemUrl = ClassLoader.getSystemResource(resource);
					if (systemUrl == null) throw new NoSuchElementException("No such system resource: " + resource);

					url = systemUrl.toExternalForm();
				}
				put(name, url);
			}
			log.info("Parsed sound entry: {}", name);
		}
	}
	/**
	 * Adds a {@code name -> resource} association.
	 * @param name sound name
	 * @param resource resource URL
	 */
	public void put(String name, String resource) {
		List<String> resources = resourceLists.computeIfAbsent(name, k -> {
			log.info("No resource list for name={}, creating new...", name);
			return new ArrayList<>();
		});
		resources.add(resource);
	}

	/** @return maximum size of audio file cache */<commit_msg>Pancake-20: Change resource lists to use WeightedDistribution<commit_after>package dev.kkorolyov.pancake.sound;

import dev.kkorolyov.pancake.Config;
import dev.kkorolyov.pancake.math.WeightedDistribution;
import dev.kkorolyov.simplelogs.Logger;
import dev.kkorolyov.simpleprops.Properties;

import javafx.scene.media.AudioClip;
import java.net.URL;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.NoSuchElementException;

/**
 * Provides access to sounds by name.
 * Names are mapped to an arbitrary-length collection of sound resources.
 * When retrieving from a name with multiple resource associations, a randomly-selected associated resource is returned.
 */
public class SoundPool {
	private static final int DEFAULT_CACHE_SIZE = 15;
	private static final Logger log = Config.getLogger(SoundPool.class);

	private final Map<String, WeightedDistribution<String>> resourceDistributions = new HashMap<>();
	private final Map<String, AudioClip> cache;
	private int cacheSize;

	/**
	 * Constructs a new sound pool using the {@link #DEFAULT_CACHE_SIZE}.
	 */
	public SoundPool() {
		this(DEFAULT_CACHE_SIZE);
	}
	/**
	 * Constructs a new sound pool using some cache size.
	 * @param cacheSize maximum size of audio file cache
	 */
	public SoundPool(int cacheSize) {
		cache = new LinkedHashMap<String, AudioClip>(cacheSize) {
			@Override
			protected boolean removeEldestEntry(Entry eldest) {
				return size() > getCacheSize();
			}
		};
		setCacheSize(cacheSize);

		log.debug("Constructed a new SoundPool with cacheSize={}", getCacheSize());
	}

	/**
	 * Retrieves a sound randomly selected from all resources associated with a name.
	 * @param name sound name
	 * @return randomly-selected sound associated with {@code name}
	 * @throws NoSuchElementException if no sounds associated with {@code name}
	 */
	public AudioClip get(String name) {
		return cache.computeIfAbsent(getResource(name), AudioClip::new);
	}
	private String getResource(String name) {
		WeightedDistribution<String> resources = resourceDistributions.get(name);
		if (resources == null) throw new NoSuchElementException("No resources associated with name: " + name);

		return resources.get();
	}

	/**
	 * Parses sounds from a configuration file.
	 * Each entry is a sound name mapped to an arbitrary-length list of resources, all with weight {@code 1}.
	 * If a resource does not contain {@code //}, it is assumed to be found on the local filesystem.
	 * @param soundConfig sound configuration file
	 */
	public void put(Properties soundConfig) {
		for (String name : soundConfig.keys()) {
			for (String resource : soundConfig.getArray(name)) {
				String url = resource;

				if (!resource.contains("//")) {
					URL systemUrl = ClassLoader.getSystemResource(resource);
					if (systemUrl == null) throw new NoSuchElementException("No such system resource: " + resource);

					url = systemUrl.toExternalForm();
				}
				put(name, url);
			}
			log.info("Parsed sound entry: {}", name);
		}
	}

	/**
	 * Adds a {@code name -> resource} association with weight {@code 1}.
	 * @param name sound name
	 * @param resource resource URL
	 */
	public void put(String name, String resource) {
		put(name, resource, 1);
	}
	/**
	 * Adds a {@code name -> resource} association.
	 * @param name sound name
	 * @param resource resource URL
	 * @param weight {@code resource} weight compared to other resources in the same distribution
	 */
	public void put(String name, String resource, int weight) {
		WeightedDistribution<String> resources = resourceDistributions.computeIfAbsent(name, k -> {
			log.info("No resource list for name={}, creating new...", name);
			return new WeightedDistribution<>();
		});
		resources.add(weight, resource);
	}

	/** @return maximum size of audio file cache */
{'fraction_non_alphanumeric': 0.10695187165775401, 'fraction_numerical': 0.0012340600575894694, 'mean_word_length': 4.255043227665706, 'pattern_counts': {'":': 0, '<': 19, '<?xml version=': 0, '>': 26, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '7152554', 'n_tokens_mistral': 2199, 'n_tokens_neox': 2006, 'n_words': 701}