// Copyright 2012 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package com.google.collide.client.code.debugging;

import collide.client.treeview.TreeNodeElement;

import com.google.collide.client.code.debugging.DebuggerApiTypes.RemoteObject;
import com.google.collide.client.code.debugging.DebuggerApiTypes.RemoteObjectSubType;
import com.google.collide.client.code.debugging.DebuggerApiTypes.RemoteObjectType;
import com.google.collide.json.shared.JsonArray;
import com.google.collide.shared.util.JsonCollections;
import com.google.collide.shared.util.SortedList;
import com.google.collide.shared.util.StringUtils;
import com.google.gwt.regexp.shared.MatchResult;
import com.google.gwt.regexp.shared.RegExp;

/**
 * Represents a {@link RemoteObject} node in the tree UI.
 *
 */
class RemoteObjectNode implements Comparable<RemoteObjectNode> {

  private static final String GETTER_PROPERTY_PREFIX = "get ";
  private static final String SETTER_PROPERTY_PREFIX = "set ";
  private static final String PROTO_PROPERTY_NAME = "__proto__";
  private static final RegExp CHUNK_FROM_BEGINNING = RegExp.compile("(^\\d+)|(^\\D+)");

  private static final SortedList.Comparator<RemoteObjectNode> SORTING_FUNCTION =
      new SortedList.Comparator<RemoteObjectNode>() {
        @Override
        public int compare(RemoteObjectNode a, RemoteObjectNode b) {
          return a.compareTo(b);
        }
      };

  private String name;
  private final int orderIndex;
  private final SortedList<RemoteObjectNode> children;
  private final RemoteObject remoteObject;
  private final boolean wasThrown;
  private final boolean isDeletable;
  private final boolean isWritable;
  private final boolean isEnumerable;
  private final String getterOrSetterName;
  private final boolean isTransient;
  private boolean shouldRequestChildren;

  public static RemoteObjectNode createRoot() {
    return new Builder("/")
        .setHasChildren(true)
        .setDeletable(false)
        .build();
  }

  public static RemoteObjectNode createGetterProperty(String name, RemoteObject getterFunction) {
    return new Builder(GETTER_PROPERTY_PREFIX + name, getterFunction)
        .setGetterOrSetterName(name)
        .build();
  }

  public static RemoteObjectNode createSetterProperty(String name, RemoteObject setterFunction) {
    return new Builder(SETTER_PROPERTY_PREFIX + name, setterFunction)
        .setGetterOrSetterName(name)
        .build();
  }

  public static RemoteObjectNode createBeingEdited() {
    return new Builder("")
        .setOrderIndex(Integer.MAX_VALUE)
        .build();
  }

  private static RemoteObjectNode createNoPropertiesPlaceholder() {
    // TODO: i18n?
    return new Builder("No Properties")
        .setDeletable(false)
        .setWritable(false)
        .build();
  }

  private RemoteObjectNode(Builder builder) {
    this.name = builder.name;
    this.orderIndex = builder.orderIndex;
    this.children = (builder.hasChildren && !builder.wasThrown) ?
        new SortedList<RemoteObjectNode>(SORTING_FUNCTION) : null;
    this.remoteObject = builder.remoteObject;
    this.wasThrown = builder.wasThrown;
    this.getterOrSetterName = builder.getterOrSetterName;
    this.isTransient = builder.isTransient;

    boolean isDeletable = builder.isDeletable;
    boolean isWritable = builder.isWritable;
    boolean isEnumerable = builder.isEnumerable;

    if (PROTO_PROPERTY_NAME.equals(name)) {
      // The __proto__ property can not be deleted, although can be changed.
      isDeletable = false;
      isEnumerable = false;
    }
    if (getterOrSetterName != null) {
      // TODO: Maybe allow editing and/or deleting getters and setters?
      isDeletable = false;
      isWritable = false;
    }

    this.isDeletable = isDeletable;
    this.isWritable = isWritable;
    this.isEnumerable = isEnumerable;

    this.shouldRequestChildren = (!wasThrown && remoteObject != null && remoteObject.hasChildren());
  }

  /**
   * Tears down the object in order to prevent leaks. Do not use the object once
   * this method is called.
   */
  public void teardown() {
    if (children != null) {
      children.clear();
    }
    setParent(null);
    setRenderedTreeNode(null);
  }

  public String getName() {
    return name;
  }

  public void setName(String name) {
    RemoteObjectNode parent = getParent();
    if (parent != null) {
      parent.removeChild(this);
    }
    this.name = name;
    if (parent != null) {
      parent.addChild(this);
    }
  }

  public int getOrderIndex() {
    return orderIndex;
  }

  public String getNodeId() {
    return name + "#" + orderIndex;
  }

  public native final RemoteObjectNode getParent() /*-{
    return this.__parentRef;
  }-*/;

  private native void setParent(RemoteObjectNode parent) /*-{
    this.__parentRef = parent;
  }-*/;

  public boolean hasChildren() {
    return children != null;
  }

  public boolean wasThrown() {
    return wasThrown;
  }

  public boolean canAddRemoteObjectProperty() {
    if (isTransient || remoteObject == null || !hasChildren()) {
      return false;
    }

    RemoteObjectType type = remoteObject.getType();
    RemoteObjectSubType subType = remoteObject.getSubType();

    return type == RemoteObjectType.FUNCTION
        || (type == RemoteObjectType.OBJECT && subType != RemoteObjectSubType.NULL);
  }

  /**
   * @return true if this property can be deleted from the parent object
   */
  public boolean isDeletable() {
    return isDeletable;
  }

  /**
   * @return true if the value of this property can be changed
   */
  public boolean isWritable() {
    return isWritable;
  }

  public boolean isEnumerable() {
    return isEnumerable;
  }

  /**
   * @return true if the object represented by this node refers to an artificial
   *         transient remote object
   * @see DebuggerApiTypes.Scope#isTransient
   */
  public boolean isTransient() {
    return isTransient;
  }

  public boolean shouldRequestChildren() {
    return shouldRequestChildren && hasChildren();
  }

  public void setAllChildrenRequested() {
    shouldRequestChildren = false;
  }

  public JsonArray<RemoteObjectNode> getChildren() {
    if (children == null) {
      return JsonCollections.createArray();
    }
    // Some remote objects may have no children. In this case we return a
    // special "placeholder" child to display this fact in the UI.
    if (children.size() == 0 && remoteObject != null && !shouldRequestChildren) {
      addChild(createNoPropertiesPlaceholder());
    }
    return children.toArray(); // Returns copy.
  }

  public RemoteObject getRemoteObject() {
    return remoteObject;
  }

  /**
   * @return the associated rendered {@link TreeNodeElement}. If there is no
   *         tree node element rendered yet, then {@code null} is returned
   */
  public final native TreeNodeElement<RemoteObjectNode> getRenderedTreeNode() /*-{
    return this.__renderedNode;
  }-*/;

  /**
   * Associates this RemoteObjectNode with the supplied {@link TreeNodeElement}
   * as the rendered node in the tree. This allows us to go from model ->
   * rendered tree element in order to reflect model mutations in the tree.
   */
  public final native void setRenderedTreeNode(TreeNodeElement<RemoteObjectNode> renderedNode) /*-{
    this.__renderedNode = renderedNode;
  }-*/;

  public boolean isRootChild() {
    return getParent() != null && getParent().getParent() == null;
  }

  @Override
  public int compareTo(RemoteObjectNode that) {
    if (this == that) {
      return 0;
    }

    int orderIndexDiff = this.orderIndex - that.orderIndex;
    if (orderIndexDiff != 0) {
      return orderIndexDiff;
    }

    String a = this.getName();
    String b = that.getName();

    if (PROTO_PROPERTY_NAME.equals(a)) {
      return 1;
    }
    if (PROTO_PROPERTY_NAME.equals(b)) {
      return -1;
    }

    // Sort by digits/non-digits chunks.
    while (true) {
      boolean emptyA = StringUtils.isNullOrEmpty(a);
      boolean emptyB = StringUtils.isNullOrEmpty(b);

      if (emptyA && emptyB) {
        return 0;
      }
      if (emptyA && !emptyB) {
        return -1;
      }
      if (emptyB && !emptyA) {
        return 1;
      }

      MatchResult resultA = CHUNK_FROM_BEGINNING.exec(a);
      MatchResult resultB = CHUNK_FROM_BEGINNING.exec(b);

      String chunkA = resultA.getGroup(0);
      String chunkB = resultB.getGroup(0);

      boolean isNumA = !StringUtils.isNullOrEmpty(resultA.getGroup(1));
      boolean isNumB = !StringUtils.isNullOrEmpty(resultB.getGroup(1));
      if (isNumA && !isNumB) {
        return -1;
      }
      if (isNumB && !isNumA) {
        return 1;
      }
      if (isNumA && isNumB) {
        // Must be parseDouble to handle big integers.
        double valueA = Double.parseDouble(chunkA);
        double valueB = Double.parseDouble(chunkB);

        if (valueA != valueB) {
          return valueA < valueB ? -1 : 1;
        }

        int diff = chunkA.length() - chunkB.length();
        if (diff != 0) {
          if (valueA == 0) {
            // "file_0" should precede "file_00".
            return diff;
          } else {
            // "file_015" should precede "file_15".
            return -diff;
          }
        }
      } else {
        int diff = chunkA.compareTo(chunkB);
        if (diff != 0) {
          return diff;
        }
      }

      a = a.substring(chunkA.length());
      b = b.substring(chunkB.length());
    }
  }

  public void addChild(RemoteObjectNode remoteObjectNode) {
    assert (hasChildren()) : "Adding children to a leaf node is not allowed!";

    remoteObjectNode.setParent(this);
    children.add(remoteObjectNode);
  }

  public void removeChild(RemoteObjectNode remoteObjectNode) {
    assert (hasChildren()) : "Removing a child from a leaf node?!";

    children.remove(remoteObjectNode);
  }

  public RemoteObjectNode getFirstChildByName(String name) {
    if (children == null) {
      return null;
    }

    for (int i = 0, n = children.size(); i < n; ++i) {
      RemoteObjectNode child = children.get(i);
      if (name.equals(child.getName())) {
        return child;
      }
    }

    return null;
  }

  public RemoteObjectNode getLastChild() {
    if (children == null || children.size() == 0) {
      return null;
    }
    return children.get(children.size() - 1);
  }

  /**
   * Builder class for the {@link RemoteObjectNode}.
   */
  public static class Builder {
    private final String name;
    private final RemoteObject remoteObject;
    private int orderIndex;
    private boolean hasChildren;
    private boolean wasThrown;
    private boolean isDeletable = true;
    private boolean isWritable = true;
    private boolean isEnumerable = true;
    private boolean isTransient;
    private String getterOrSetterName;

    public Builder(String name) {
      this(name, null);
    }

    public Builder(String name, RemoteObject remoteObject) {
      this(name, remoteObject, null);
    }

    public Builder(String name, RemoteObject remoteObject, RemoteObjectNode proto) {
      this.name = name;
      this.remoteObject = remoteObject;
      this.hasChildren = (remoteObject != null && remoteObject.hasChildren());

      if (proto != null) {
        this.orderIndex = proto.orderIndex;
        this.wasThrown = proto.wasThrown;
        this.isDeletable = proto.isDeletable;
        this.isWritable = proto.isWritable;
        this.isEnumerable = proto.isEnumerable;
        this.isTransient = proto.isTransient;
        this.getterOrSetterName = proto.getterOrSetterName;
      }
    }

    public Builder setOrderIndex(int orderIndex) {
      this.orderIndex = orderIndex;
      return this;
    }

    public Builder setHasChildren(boolean hasChildren) {
      this.hasChildren = hasChildren;
      return this;
    }

    public Builder setWasThrown(boolean wasThrown) {
      this.wasThrown = wasThrown;
      return this;
    }

    public Builder setDeletable(boolean deletable) {
      isDeletable = deletable;
      return this;
    }

    public Builder setWritable(boolean writable) {
      isWritable = writable;
      return this;
    }

    public Builder setEnumerable(boolean enumerable) {
      isEnumerable = enumerable;
      return this;
    }

    public Builder setTransient(boolean aTransient) {
      this.isTransient = aTransient;
      return this;
    }

    private Builder setGetterOrSetterName(String name) {
      getterOrSetterName = name;
      return this;
    }

    public RemoteObjectNode build() {
      return new RemoteObjectNode(this);
    }
  }
}
