public void insert(char[] origChars, int off, V value) {
    // precondition: origChars and this node share a common prefix
    // off is positioned at the first character that does not match this node's
    // prefix.

    // if there are no children, then add a new single child with all of the chars.
    if (children == null) {
      children = new MutableNode[] {
        new MutableNode(origChars, off, origChars.length - off, value)
      };
      return;
    }

    // there are existing children. scan them to see if there are any with
    // matching prefixes
    for (int i = 0; i < children.length; i++) {
      MutableNode childNode = children[i];
      int commonLength = Utils.getCommonLength(origChars, off, childNode.chars);
      if (commonLength == 0) {
        // no match. continue on.
        continue;
      }

      if (commonLength < childNode.chars.length) {
        // the child node and the new string share a prefix, but the child node
        // is currently longer than the match length. we need to split the child
        // node.

        MutableNode replacement = new MutableNode(childNode.chars, 0, commonLength, null);
        final MutableNode newChild = new MutableNode(childNode.chars, commonLength, childNode.chars.length - commonLength, childNode.value);
        if (childNode.children != null) {
          newChild.children = new MutableNode[childNode.children.length];
          System.arraycopy(childNode.children, 0, newChild.children, 0, childNode.children.length);
        }

        replacement.children = new MutableNode[] {
            newChild
        };
        children[i] = replacement;

        // if we still have any chars left, recurse
        if (commonLength < origChars.length - off) {
          replacement.insert(origChars, off + commonLength, value);
        } else {
          // the split used up the available chars, so we're here!
          replacement.value = value;
        }
        return;
      }

      if (commonLength == origChars.length - off && commonLength == childNode.chars.length) {
        // it's an exact match. replace.
        childNode.value = value;
        return;
      }

      // we match on the whole length. recursively insert.
      childNode.insert(origChars, off + commonLength, value);
      return;
    }

    // if we get here, then we never found *any* match. insert a new child node.
    MutableNode[] oldChildren = children;
    children = new MutableNode[oldChildren.length + 1];
    System.arraycopy(oldChildren, 0, children, 0, oldChildren.length);
    children[children.length - 1] = new MutableNode(origChars, off, origChars.length - off, value);
  }