package name.abuchen.portfolio.ui.views.taxonomy;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import javax.inject.Inject;
import name.abuchen.portfolio.model.Account;
import name.abuchen.portfolio.model.Classification;
import name.abuchen.portfolio.model.Classification.Assignment;
import name.abuchen.portfolio.model.Client;
import name.abuchen.portfolio.model.InvestmentVehicle;
import name.abuchen.portfolio.model.Security;
import name.abuchen.portfolio.model.Taxonomy;
import name.abuchen.portfolio.money.CurrencyConverter;
import name.abuchen.portfolio.money.CurrencyConverterImpl;
import name.abuchen.portfolio.money.ExchangeRateProviderFactory;
import name.abuchen.portfolio.money.Money;
import name.abuchen.portfolio.money.MutableMoney;
import name.abuchen.portfolio.snapshot.AssetPosition;
import name.abuchen.portfolio.snapshot.ClientSnapshot;
import name.abuchen.portfolio.ui.Messages;
import name.abuchen.portfolio.ui.views.taxonomy.TaxonomyNode.AssignmentNode;
import name.abuchen.portfolio.ui.views.taxonomy.TaxonomyNode.ClassificationNode;
import name.abuchen.portfolio.ui.views.taxonomy.TaxonomyNode.UnassignedContainerNode;
public final class TaxonomyModel
{
@FunctionalInterface
public interface NodeVisitor
{
void visit(TaxonomyNode node);
}
@FunctionalInterface
public interface TaxonomyModelUpdatedListener
{
void nodeChange(TaxonomyNode node);
}
@FunctionalInterface
public interface DirtyListener
{
void onModelEdited();
}
private final Taxonomy taxonomy;
private ClientSnapshot snapshot;
private final CurrencyConverter converter;
private TaxonomyNode virtualRootNode;
private TaxonomyNode classificationRootNode;
private TaxonomyNode unassignedNode;
private Map<InvestmentVehicle, Assignment> investmentVehicle2weight = new HashMap<>();
private boolean excludeUnassignedCategoryInCharts = false;
private boolean orderByTaxonomyInStackChart = false;
private String expansionStateDefinition;
private String expansionStateRebalancing;
private List<TaxonomyModelUpdatedListener> listeners = new ArrayList<>();
private List<DirtyListener> dirtyListener = new ArrayList<>();
@Inject
/* package */ TaxonomyModel(ExchangeRateProviderFactory factory, Client client, Taxonomy taxonomy)
{
Objects.requireNonNull(client);
Objects.requireNonNull(taxonomy);
this.taxonomy = taxonomy;
this.converter = new CurrencyConverterImpl(factory, client.getBaseCurrency());
this.snapshot = ClientSnapshot.create(client, converter, LocalDate.now());
Classification virtualRoot = new Classification(null, Classification.VIRTUAL_ROOT,
Messages.PerformanceChartLabelEntirePortfolio, taxonomy.getRoot().getColor());
virtualRootNode = new ClassificationNode(null, virtualRoot);
Classification classificationRoot = taxonomy.getRoot();
classificationRootNode = new ClassificationNode(virtualRootNode, classificationRoot);
virtualRootNode.getChildren().add(classificationRootNode);
LinkedList<TaxonomyNode> stack = new LinkedList<>();
stack.add(classificationRootNode);
while (!stack.isEmpty())
{
TaxonomyNode m = stack.pop();
Classification classification = m.getClassification();
for (Classification c : classification.getChildren())
{
TaxonomyNode cm = new ClassificationNode(m, c);
stack.push(cm);
m.getChildren().add(cm);
}
for (Assignment assignment : classification.getAssignments())
m.getChildren().add(new AssignmentNode(m, assignment));
Collections.sort(m.getChildren(), (o1, o2) -> Integer.compare(o1.getRank(), o2.getRank()));
}
unassignedNode = new UnassignedContainerNode(virtualRootNode, new Classification(virtualRoot,
Classification.UNASSIGNED_ID, Messages.LabelWithoutClassification));
virtualRootNode.getChildren().add(unassignedNode);
// add unassigned
addUnassigned(client);
// calculate actuals
visitActuals(snapshot, virtualRootNode);
// calculate targets
recalculateTargets();
}
private void addUnassigned(Client client)
{
for (Security security : client.getSecurities())
{
Assignment assignment = new Assignment(security);
assignment.setWeight(0);
investmentVehicle2weight.put(security, assignment);
}
for (Account account : client.getAccounts())
{
Assignment assignment = new Assignment(account);
assignment.setWeight(0);
investmentVehicle2weight.put(account, assignment);
}
visitAll(node -> {
if (!(node instanceof AssignmentNode))
return;
Assignment assignment = node.getAssignment();
Assignment count = investmentVehicle2weight.get(assignment.getInvestmentVehicle());
count.setWeight(count.getWeight() + assignment.getWeight());
});
List<Assignment> unassigned = new ArrayList<>();
for (Assignment assignment : investmentVehicle2weight.values())
{
if (assignment.getWeight() >= Classification.ONE_HUNDRED_PERCENT)
continue;
Assignment a = new Assignment(assignment.getInvestmentVehicle());
a.setWeight(Classification.ONE_HUNDRED_PERCENT - assignment.getWeight());
unassigned.add(a);
assignment.setWeight(Classification.ONE_HUNDRED_PERCENT);
}
Collections.sort(unassigned, (o1, o2) -> o1.getInvestmentVehicle().toString()
.compareToIgnoreCase(o2.getInvestmentVehicle().toString()));
for (Assignment assignment : unassigned)
unassignedNode.addChild(assignment);
}
private void visitActuals(ClientSnapshot snapshot, TaxonomyNode node)
{
MutableMoney actual = MutableMoney.of(snapshot.getCurrencyCode());
for (TaxonomyNode child : node.getChildren())
{
visitActuals(snapshot, child);
actual.add(child.getActual());
}
if (node.isAssignment())
{
Assignment assignment = node.getAssignment();
AssetPosition p = snapshot.getPositionsByVehicle().get(assignment.getInvestmentVehicle());
if (p != null)
{
Money valuation = p.getValuation();
actual.add(Money.of(valuation.getCurrencyCode(), Math.round(valuation.getAmount()
* assignment.getWeight() / (double) Classification.ONE_HUNDRED_PERCENT)));
}
}
node.setActual(actual.toMoney());
}
private void recalculateTargets()
{
virtualRootNode.setTarget(virtualRootNode.getActual().subtract(unassignedNode.getActual()));
visitAll(node -> {
if (node.isClassification() && !node.isRoot())
{
Money parent = node.getParent().getTarget();
Money target = Money.of(parent.getCurrencyCode(), Math.round(
parent.getAmount() * node.getWeight() / (double) Classification.ONE_HUNDRED_PERCENT));
node.setTarget(target);
}
});
}
public boolean isUnassignedCategoryInChartsExcluded()
{
return excludeUnassignedCategoryInCharts;
}
public void setExcludeUnassignedCategoryInCharts(boolean excludeUnassignedCategoryInCharts)
{
this.excludeUnassignedCategoryInCharts = excludeUnassignedCategoryInCharts;
}
public boolean isOrderByTaxonomyInStackChart()
{
return orderByTaxonomyInStackChart;
}
public void setOrderByTaxonomyInStackChart(boolean orderByTaxonomyInStackChart)
{
this.orderByTaxonomyInStackChart = orderByTaxonomyInStackChart;
}
public String getExpansionStateDefinition()
{
return expansionStateDefinition;
}
public void setExpansionStateDefinition(String expansionStateDefinition)
{
this.expansionStateDefinition = expansionStateDefinition;
}
public String getExpansionStateRebalancing()
{
return expansionStateRebalancing;
}
public void setExpansionStateRebalancing(String expansionStateRebalancing)
{
this.expansionStateRebalancing = expansionStateRebalancing;
}
public Taxonomy getTaxonomy()
{
return taxonomy;
}
/**
* Returns the virtual root node, i.e. the root node that includes the
* classification and the node with unassigned securities.
*/
public TaxonomyNode getVirtualRootNode()
{
return virtualRootNode;
}
/**
* Returns the root node of classifications, i.e. the part of the tree that
* includes assigned investment vehicles.
*/
public TaxonomyNode getClassificationRootNode()
{
return classificationRootNode;
}
/**
* Returns the node that holds all unassigned investment vehicles.
*/
public TaxonomyNode getUnassignedNode()
{
return unassignedNode;
}
/**
* Returns the root node that is to be rendered in charts (pie, tree map,
* stacked chart). It is the whole node tree unless unassigned investment
* vehicles are not to be included or do not exist.
*/
public TaxonomyNode getChartRenderingRootNode()
{
return isUnassignedCategoryInChartsExcluded() || getUnassignedNode().getActual().isZero()
? getClassificationRootNode() : getVirtualRootNode();
}
public Client getClient()
{
return snapshot.getClient();
}
public CurrencyConverter getCurrencyConverter()
{
return converter;
}
public String getCurrencyCode()
{
return converter.getTermCurrency();
}
public void setClientSnapshot(ClientSnapshot newSnapshot)
{
this.snapshot = newSnapshot;
recalculate();
}
public ClientSnapshot getClientSnapshot()
{
return snapshot;
}
public void recalculate()
{
virtualRootNode.setActual(snapshot.getMonetaryAssets());
visitActuals(snapshot, virtualRootNode);
recalculateTargets();
}
public void visitAll(NodeVisitor visitor)
{
virtualRootNode.accept(visitor);
}
public void addListener(TaxonomyModelUpdatedListener listener)
{
listeners.add(listener);
}
public void removeListener(TaxonomyModelUpdatedListener listener)
{
listeners.remove(listener);
}
public void fireTaxonomyModelChange(TaxonomyNode node)
{
listeners.forEach(listener -> listener.nodeChange(node));
}
public void addDirtyListener(DirtyListener listener)
{
dirtyListener.add(listener);
}
public void markDirty()
{
dirtyListener.forEach(l -> l.onModelEdited());
}
public int getWeightByInvestmentVehicle(InvestmentVehicle vehicle)
{
return investmentVehicle2weight.get(vehicle).getWeight();
}
public void setWeightByInvestmentVehicle(InvestmentVehicle vehicle, int weight)
{
investmentVehicle2weight.get(vehicle).setWeight(weight);
}
public boolean hasWeightError(TaxonomyNode node)
{
if (node.isUnassignedCategory() || node.isRoot())
{
return false;
}
else if (node.isClassification())
{
if (node.getParent().isRoot())
return node.getWeight() != Classification.ONE_HUNDRED_PERCENT;
else
return node.getClassification().getParent().getChildrenWeight() != Classification.ONE_HUNDRED_PERCENT;
}
else
{
// node is assignment
return getWeightByInvestmentVehicle(
node.getAssignment().getInvestmentVehicle()) != Classification.ONE_HUNDRED_PERCENT;
}
}
}