package io.kaif.model.debate;
import static java.util.stream.Collectors.*;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.function.ToDoubleFunction;
import java.util.stream.Stream;
import javax.annotation.concurrent.Immutable;
import io.kaif.rank.SortingNode;
import io.kaif.rank.WilsonScore;
import io.kaif.web.v1.dto.V1DebateDto;
import io.kaif.web.v1.dto.V1DebateNodeDto;
@Immutable
public final class DebateTree {
public static final ToDoubleFunction<Debate> DEBATE_TO_WILSON_SCORE = (debate) -> {
if (debate == null) {
return WilsonScore.lowerBound(0, 0);
} else {
return WilsonScore.lowerBound(debate.getUpVote(), debate.getDownVote());
}
};
/**
* construct a DebateTree from a properly sorted debate list, the sorting order must be depth
* first.
* <p>
* if input debate list is not depth first, exception thrown.
*/
public static DebateTree fromDepthFirst(List<Debate> flatten) {
if (flatten.isEmpty()) {
return new DebateTree(SortingNode.emptyRoot());
}
SortingNode.Builder<Debate> builder = new SortingNode.Builder<>();
int currentLevel = flatten.get(0).getLevel() - 1;
for (Debate debate : flatten) {
if (debate.getLevel() == currentLevel) {
builder = builder.siblingNode(debate);
} else if (debate.getLevel() == currentLevel + 1) {
builder = builder.childNode(debate);
currentLevel++;
} else if (debate.getLevel() < currentLevel) {
while (debate.getLevel() < currentLevel) {
builder = builder.parent();
currentLevel--;
}
builder = builder.siblingNode(debate);
} else {
throw new IllegalStateException("not well flatten depth first debate list");
}
}
return new DebateTree(builder.build());
}
/**
* pick max score within all sub nodes, include self.
* <p>
* choosing the node tree based on highest score of that tree, means we sort most interesting
* node to top. so user won't missing any good debate.
*/
private static double maxScoreOfAllNodes(SortingNode<Debate> node,
ToDoubleFunction<Debate> scoreCalc) {
return node.depthFirst()
.mapToDouble(scoreCalc)
.max()
.orElseGet(() -> scoreCalc.applyAsDouble(null));
}
private static V1DebateNodeDto deepDto(SortingNode<Debate> node) {
V1DebateDto currentDto = Optional.ofNullable(node.getValue()).map(Debate::toV1Dto).orElse(null);
if (!node.hasChild()) {
return new V1DebateNodeDto(currentDto, Collections.emptyList());
}
List<V1DebateNodeDto> childDto = node.getChildren()
.stream()
.map(DebateTree::deepDto)
.collect(toList());
return new V1DebateNodeDto(currentDto, childDto);
}
private final SortingNode<Debate> node;
public DebateTree(SortingNode<Debate> node) {
this.node = node;
}
public List<SortingNode<Debate>> getChildren() {
return node.getChildren();
}
public boolean isEmpty() {
return node.hasChild();
}
/**
* scoreCalc should calculate Debate's score, higher means better. the function also need to
* handle null Debate case (means lowest score).
* <p>
* for example, a naive net score calculator:
* <p>
* <pre>
* ToDoubleFunction<Debate> netScoreCalc = (debate) -> {
* if (debate == null) {
* return 0d;
* } else {
* return debate.getUpVote() - debate.getDownVote();
* }
* }
* </pre>
*
* @return new sorted DebateTree based on best score
*/
public DebateTree sortByBestScore(ToDoubleFunction<Debate> scoreCalc) {
SortingNode<Debate> sorted = node.deepSort((child1, child2) -> {
double score1 = maxScoreOfAllNodes(child1, scoreCalc);
double score2 = maxScoreOfAllNodes(child2, scoreCalc);
return Double.compare(score2, score1);
});
return new DebateTree(sorted);
}
/**
* sort tree by wilson score
* <p>
* see {@link #sortByBestScore(java.util.function.ToDoubleFunction)}
*/
public DebateTree sortByBestScore() {
return sortByBestScore(DEBATE_TO_WILSON_SCORE);
}
public Stream<Debate> depthFirst() {
return node.depthFirst();
}
public V1DebateNodeDto toV1Dto() {
return deepDto(node);
}
}