package name.abuchen.portfolio.money;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import javax.imageio.spi.ServiceRegistry;
import javax.inject.Singleton;
import org.eclipse.e4.core.di.annotations.Creatable;
import name.abuchen.portfolio.money.impl.ChainedExchangeRateTimeSeries;
import name.abuchen.portfolio.money.impl.InverseExchangeRateTimeSeries;
@Singleton
@Creatable
public class ExchangeRateProviderFactory
{
private class Dijkstra
{
private List<ExchangeRateTimeSeries> timeSeries = new ArrayList<>();
private Set<String> visited = new HashSet<>();
private Set<String> unvisited = new HashSet<>();
private Map<String, ExchangeRateTimeSeries> predecessors = new HashMap<>();
private Map<String, Integer> distance = new HashMap<>();
public Dijkstra(List<ExchangeRateTimeSeries> availableTimeSeries, String baseCurrency)
{
availableTimeSeries.stream().forEach(ts -> {
timeSeries.add(ts);
timeSeries.add(new InverseExchangeRateTimeSeries(ts));
});
distance.put(baseCurrency, 0);
unvisited = CurrencyUnit.getAvailableCurrencyUnits().stream().map(CurrencyUnit::getCurrencyCode)
.collect(Collectors.toSet());
if (!unvisited.contains(baseCurrency))
return;
while (!unvisited.isEmpty())
{
String node = getNodeWithMinimumDistance(unvisited);
unvisited.remove(node);
visited.add(node);
for (ExchangeRateTimeSeries neighbor : getNeighbors(node))
{
String neighborNode = neighbor.getTermCurrency();
int alternativeDistance = getDistance(node) + neighbor.getWeight();
if (alternativeDistance < getDistance(neighborNode))
{
distance.put(neighborNode, alternativeDistance);
predecessors.put(neighborNode, neighbor);
}
}
}
}
public List<ExchangeRateTimeSeries> findShortestPath(String termCurrency)
{
ExchangeRateTimeSeries current = predecessors.get(termCurrency);
if (current == null)
return Collections.emptyList();
LinkedList<ExchangeRateTimeSeries> answer = new LinkedList<>();
while (current != null)
{
answer.addFirst(current);
current = predecessors.get(current.getBaseCurrency());
}
return answer;
}
private List<ExchangeRateTimeSeries> getNeighbors(String node)
{
return timeSeries.stream()
.filter(ts -> ts.getBaseCurrency().equals(node) && !visited.contains(ts.getTermCurrency()))
.collect(Collectors.toList());
}
private String getNodeWithMinimumDistance(Set<String> currencies)
{
return currencies.stream().min(Comparator.comparingInt(this::getDistance))
.orElse(currencies.iterator().next());
}
private int getDistance(String currency)
{
Integer d = distance.get(currency);
return d == null ? Integer.MAX_VALUE : d;
}
}
private static class CurrencyPair
{
private final String base;
private final String term;
public CurrencyPair(String base, String term)
{
this.base = base;
this.term = term;
}
@Override
public int hashCode()
{
return Objects.hash(base, term);
}
@Override
public boolean equals(Object obj)
{
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
CurrencyPair other = (CurrencyPair) obj;
if (!Objects.equals(base, other.base))
return false;
return Objects.equals(term, other.term);
}
}
private final List<ExchangeRateProvider> providers;
private final Map<CurrencyPair, ExchangeRateTimeSeries> cache = new HashMap<>();
public ExchangeRateProviderFactory()
{
providers = new ArrayList<>();
Iterator<ExchangeRateProvider> registeredProvider = ServiceRegistry.lookupProviders(ExchangeRateProvider.class);
while (registeredProvider.hasNext())
{
ExchangeRateProvider provider = registeredProvider.next();
providers.add(provider);
}
}
public List<ExchangeRateProvider> getProviders()
{
return Collections.unmodifiableList(providers);
}
public List<ExchangeRateTimeSeries> getAvailableTimeSeries()
{
List<ExchangeRateTimeSeries> series = new ArrayList<>();
for (ExchangeRateProvider p : providers)
series.addAll(p.getAvailableTimeSeries());
return series;
}
public void clearCache()
{
cache.clear();
}
public ExchangeRateTimeSeries getTimeSeries(String baseCurrency, String termCurrency)
{
return cache.computeIfAbsent(new CurrencyPair(baseCurrency, termCurrency),
pair -> computeTimeSeries(baseCurrency, termCurrency));
}
private ExchangeRateTimeSeries computeTimeSeries(String baseCurrency, String termCurrency)
{
Dijkstra dijkstra = new Dijkstra(getAvailableTimeSeries(), baseCurrency);
List<ExchangeRateTimeSeries> answer = dijkstra.findShortestPath(termCurrency);
if (answer.isEmpty())
return null;
else if (answer.size() == 1)
return answer.get(0);
else
return new ChainedExchangeRateTimeSeries(answer.toArray(new ExchangeRateTimeSeries[0]));
}
}