package com.thoughtworks.calabash.android;
import org.codehaus.jackson.JsonNode;
import org.codehaus.jackson.map.ObjectMapper;
import org.jruby.RubyArray;
import org.jruby.RubyHash;
import java.io.IOException;
import java.net.MalformedURLException;
import java.util.*;
import static com.thoughtworks.calabash.android.CalabashLogger.error;
import static com.thoughtworks.calabash.android.CalabashLogger.info;
public class TreeBuilder {
private final CalabashWrapper calabashWrapper;
private final CalabashHttpClient calabashHttpClient;
private final TreeNodeBuilder treeNodeBuilder;
private ObjectMapper mapper = new ObjectMapper();
public TreeBuilder(CalabashWrapper calabashWrapper) {
this.calabashWrapper = calabashWrapper;
this.calabashHttpClient = new CalabashHttpClient(calabashWrapper);
this.treeNodeBuilder = new TreeNodeBuilder(calabashWrapper);
}
public TreeBuilder(CalabashWrapper calabashWrapper, CalabashHttpClient calabashHttpClient, TreeNodeBuilder treeNodeBuilder) {
this.calabashWrapper = calabashWrapper;
this.calabashHttpClient = calabashHttpClient;
this.treeNodeBuilder = treeNodeBuilder;
}
public TreeNode createTreeFrom(UIElement root) throws CalabashException {
Set<UIElement> inspectedElements = new HashSet<UIElement>();
String elementQuery = root.getQuery();
String descendantQuery = elementQuery + " descendant *";
RubyArray descendants = calabashWrapper.query(descendantQuery);
return getTreeNodes(descendants, root, descendantQuery, inspectedElements).get(0);
}
private List<TreeNode> getTreeNodes(RubyArray allElements, UIElement root, String baseQuery, Set<UIElement> inspectedElements) throws CalabashException {
List<TreeNode> roots = new ArrayList<TreeNode>();
for (int i = allElements.size() - 1; i >= 0; i--) {
final String query = String.format(baseQuery + " index:%d", i);
RubyHash rubyElement = (RubyHash) allElements.get(i);
UIElement currentElement = new UIElement(rubyElement, query, calabashWrapper);
List<UIElement> uiElements = new ArrayList<UIElement>();
if (inspectedElements.contains(currentElement))
continue;
uiElements.add(currentElement);
uiElements.addAll(getAncestors(query, root));
merge(uiElements, roots);
inspectedElements.addAll(uiElements);
}
return roots;
}
private List<UIElement> getAncestors(String query, UIElement root) throws CalabashException {
return root == null ? getAllAncestors(query) : getAncestorsWithRoot(query, root);
}
private List<UIElement> getAllAncestors(String query) throws CalabashException {
String parentQuery = query + " parent *";
RubyArray ancestors = calabashWrapper.query(parentQuery);
return convertToList(ancestors, parentQuery);
}
private List<UIElement> getAncestorsWithRoot(String query, UIElement root) throws CalabashException {
List<UIElement> ancestorElements = getAllAncestors(query);
List<UIElement> finalElements = new ArrayList<UIElement>();
if (!ancestorElements.contains(root)) {
// when query's element is the root it won't be there in ancestor list
return finalElements;
}
for (UIElement ancestor : ancestorElements) {
finalElements.add(ancestor);
if (ancestor.equals(root)) {
return finalElements;
}
}
return finalElements;
}
private List<UIElement> convertToList(RubyArray ancestors, String baseQuery) {
List<UIElement> uiElements = new ArrayList<UIElement>();
for (int i = 0; i < ancestors.size(); i++) {
UIElement uiElement = new UIElement((RubyHash) ancestors.get(i), baseQuery + " index:" + i, calabashWrapper);
uiElements.add(uiElement);
}
return uiElements;
}
public void merge(List<UIElement> elements, List<TreeNode> roots) {
Collections.reverse(elements);
if (roots.isEmpty()) {
TreeNode root = createBranch(elements);
roots.add(root);
} else {
for (TreeNode root : roots) {
if (tryMerge(root, elements))
return;
}
TreeNode newRoot = createBranch(elements);
roots.add(0, newRoot);
}
}
private boolean tryMerge(TreeNode root, List<UIElement> elements) {
TreeNode current = root;
if (!current.getData().equals(elements.get(0))) {
return false;
}
int i;
int matchedChildIndex;
for (i = 1; i < elements.size(); i++) {
matchedChildIndex = matchWith(current.getChildren(), elements.get(i));
if (matchedChildIndex == -1) {
TreeNode newNode = createBranch(elements.subList(i, elements.size()));
current.addChild(newNode);
return true;
}
current = current.getChildren().get(matchedChildIndex);
}
return true;
}
private TreeNode createBranch(List<UIElement> elements) {
TreeNode startNode = new TreeNode();
TreeNode current = startNode;
for (int i = 0; i < elements.size(); i++) {
current.setData(elements.get(i));
if (i + 1 < elements.size()) {
TreeNode childNode = new TreeNode(elements.get(i + 1));
current.addChild(childNode);
current = childNode;
}
}
return startNode;
}
private int matchWith(List<TreeNode> elements, UIElement elementToMatch) {
for (int i = 0; i < elements.size(); i++) {
if (elements.get(i).getData().equals(elementToMatch))
return i;
}
return -1;
}
public List<TreeNode> createTree() {
List<TreeNode> treeNodes = null;
try {
info("Fetching view hierarchy");
treeNodes = new ArrayList<TreeNode>();
final JsonNode jsonNode = mapper.readTree(calabashHttpClient.getViewDump());
final JsonNode childNodes = jsonNode.get("children");
if (childNodes == null) {
return treeNodes;
}
JsonNode rootJsonNode = childNodes.get(0);
final TreeNode rootTreeNode = treeNodeBuilder.buildFrom(rootJsonNode, "* index:0");
addChildren(rootTreeNode, rootJsonNode);
treeNodes.add(rootTreeNode);
} catch (MalformedURLException e) {
error("malformed url", e);
} catch (IOException e) {
error("exception while fetching view hierarchy", e);
}
info("Done fetching view hierarchy");
return treeNodes;
}
private void addChildren(TreeNode treeNode, JsonNode jsonNode) throws IOException {
int i = 0;
final Iterator<JsonNode> children = jsonNode.get("children").getElements();
while (children.hasNext()) {
final JsonNode childJsonNode = children.next();
if (childJsonNode.get("visible").getBooleanValue()) {
final String query = treeNode.getData().getQuery() + " child * index:" + i;
final TreeNode childTreeNode = treeNodeBuilder.buildFrom(childJsonNode, query);
addChildren(childTreeNode, childJsonNode);
treeNode.appendChild(childTreeNode);
i++;
}
}
}
}