/*
 * Copyright 2018, OpenCensus Authors
 *
 * 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 io.opencensus.contrib.zpages;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Charsets;
import com.google.common.base.Splitter;
import com.google.common.base.Strings;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import io.opencensus.common.Function;
import io.opencensus.common.Functions;
import io.opencensus.common.Timestamp;
import io.opencensus.stats.Aggregation;
import io.opencensus.stats.Aggregation.Count;
import io.opencensus.stats.Aggregation.Distribution;
import io.opencensus.stats.Aggregation.LastValue;
import io.opencensus.stats.Aggregation.Sum;
import io.opencensus.stats.AggregationData;
import io.opencensus.stats.AggregationData.CountData;
import io.opencensus.stats.AggregationData.DistributionData;
import io.opencensus.stats.AggregationData.LastValueDataDouble;
import io.opencensus.stats.AggregationData.LastValueDataLong;
import io.opencensus.stats.AggregationData.SumDataDouble;
import io.opencensus.stats.AggregationData.SumDataLong;
import io.opencensus.stats.Measure;
import io.opencensus.stats.View;
import io.opencensus.stats.ViewData;
import io.opencensus.stats.ViewManager;
import io.opencensus.tags.TagKey;
import io.opencensus.tags.TagValue;
import java.io.BufferedWriter;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.time.Instant;
import java.util.Date;
import java.util.Formatter;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.SortedMap;
import javax.annotation.concurrent.GuardedBy;

/*>>>
import org.checkerframework.checker.nullness.qual.Nullable;
*/

/** HTML page formatter for all exported {@link View}s. */
@SuppressWarnings({
  "deprecation",

  // This library is not supposed to be Android or Java 7 compatible.
  "AndroidJdkLibsChecker",
  "Java7ApiChecker"
})
final class StatszZPageHandler extends ZPageHandler {

  private static final Object monitor = new Object();

  private final ViewManager viewManager;

  // measures, cachedViews and root are created when StatszZPageHandler is initialized, and will
  // be updated every time when there's a new View from viewManager.getAllExportedViews().
  // viewManager.getAllExportedViews() will be called every time when the StatsZ page is
  // re-rendered, like refreshing or navigating to other paths.

  @GuardedBy("monitor")
  private final Map<String, Measure> measures = Maps.newTreeMap();

  @GuardedBy("monitor")
  private final Set<View> cachedViews = Sets.newHashSet();

  @GuardedBy("monitor")
  private final TreeNode root = new TreeNode();

  @VisibleForTesting static final String QUERY_PATH = "path";
  private static final String STATSZ_URL = "/statsz";
  private static final String CLASS_LARGER_TR = "directory-tr";
  private static final String TABLE_HEADER_VIEW = "View Name";
  private static final String TABLE_HEADER_DESCRIPTION = "Description";
  private static final String TABLE_HEADER_MEASURE = "Measure";
  private static final String TABLE_HEADER_AGGREGATION = "Aggregation Type";
  private static final String TABLE_HEADER_START = "Start Time";
  private static final String TABLE_HEADER_END = "End Time";
  private static final String TABLE_HEADER_UNIT = "Unit";
  private static final String TABLE_HEADER_MEASURE_TYPE = "Type";
  private static final String TABLE_HEADER_SUM = "Sum";
  private static final String TABLE_HEADER_COUNT = "Count";
  private static final String TABLE_HEADER_MEAN = "Mean";
  private static final String TABLE_HEADER_MAX = "Max";
  private static final String TABLE_HEADER_MIN = "Min";
  private static final String TABLE_HEADER_DEV = "Sum of Squared Deviations";
  private static final String TABLE_HEADER_HISTOGRAM = "Histogram";
  private static final String TABLE_HEADER_RANGE = "Range";
  private static final String TABLE_HEADER_BUCKET_SIZE = "Bucket Size";
  private static final String TABLE_HEADER_LAST_VALUE = "Last Value";
  private static final long MILLIS_PER_SECOND = 1000;
  private static final long NANOS_PER_MILLISECOND = 1000 * 1000;
  private static final Splitter PATH_SPLITTER = Splitter.on('/');

  @Override
  public String getUrlPath() {
    return STATSZ_URL;
  }

  @Override
  public void emitHtml(Map<String, String> queryMap, OutputStream outputStream) {
    PrintWriter out =
        new PrintWriter(new BufferedWriter(new OutputStreamWriter(outputStream, Charsets.UTF_8)));
    out.write("<!DOCTYPE html>\n");
    out.write("<html lang=\"en\"><head>\n");
    out.write("<meta charset=\"utf-8\">\n");
    out.write("<title>StatsZ</title>\n");
    out.write("<link rel=\"shortcut icon\" href=\"https://opencensus.io/images/favicon.ico\"/>\n");
    out.write(
        "<link href=\"https://fonts.googleapis.com/css?family=Open+Sans:300\""
            + "rel=\"stylesheet\">\n");
    out.write(
        "<link href=\"https://fonts.googleapis.com/css?family=Roboto\"" + "rel=\"stylesheet\">\n");
    Formatter formatter = new Formatter(out, Locale.US);
    emitStyles(out, formatter);
    out.write("</head>\n");
    out.write("<body>\n");
    try {
      emitHtmlBody(queryMap, out, formatter);
    } catch (Throwable t) {
      out.write("Errors while generate the HTML page " + t);
    }
    out.write("</body>\n");
    out.write("</html>\n");
    out.close();
  }

  private static void emitStyles(PrintWriter out, Formatter formatter) {
    out.write("<style>");
    out.write(Style.style);
    formatter.format(".%s{font-size:150%%}", CLASS_LARGER_TR);
    out.write("</style>");
  }

  private void emitHtmlBody(Map<String, String> queryMap, PrintWriter out, Formatter formatter) {
    synchronized (monitor) {
      groupViewsByDirectoriesAndGetMeasures(
          viewManager.getAllExportedViews(), root, measures, cachedViews);
      out.write(
          "<p class=\"header\">"
              + "<img class=\"oc\" src=\"https://opencensus.io/img/logo-sm.svg\" />"
              + "Open<span>Census</span></p>");
      out.write(
          "<link href=\"https://fonts.googleapis.com/css?family=Open+Sans:300\""
              + "rel=\"stylesheet\">\n");
      out.write(
          "<link href=\"https://fonts.googleapis.com/css?family=Roboto\""
              + "rel=\"stylesheet\">\n");
      out.write("<h1><a href='?'>StatsZ</a></h1>");
      out.write("<p></p>");
      String path = queryMap.get(QUERY_PATH);
      TreeNode current = findNode(path);
      emitDirectoryTable(current, path, out, formatter);
      if (current != null && current.viewName != null) {
        ViewData viewData = viewManager.getView(current.viewName);
        emitViewData(viewData, current.viewName, out, formatter);
      }
      emitMeasureTable(measures, out, formatter);
    }
  }

  // Parses view names, creates a tree that represents the directory structure and put each view
  // under appropriate directory. Also gets measures from the given views.
  // Directories are the namespaces in view name, separated by '/'.
  private static void groupViewsByDirectoriesAndGetMeasures(
      Set<View> views, TreeNode root, Map<String, Measure> measures, Set<View> cachedViews) {
    for (View view : views) {
      if (cachedViews.contains(view)) {
        continue;
      }
      cachedViews.add(view);

      List<String> dirs = PATH_SPLITTER.splitToList(view.getName().asString());
      TreeNode node = root;
      for (int i = 0; i < dirs.size(); i++) {
        if (node == null) {
          break; // Should never happen. Work around the nullness checker.
        }
        String dir = dirs.get(i);
        if ("".equals(dir) && i == 0) {
          continue; // In case view name starts with a '/'.
        }
        node.views++;
        if (i != dirs.size() - 1) { // Non-leaf node (directory node)
          node.children.putIfAbsent(dir, new TreeNode());
          node = node.children.get(dir);
        } else { // Leaf node (view node)
          node.children.putIfAbsent(dir, new TreeNode(view.getName()));
        }
      }

      Measure measure = view.getMeasure();
      measures.putIfAbsent(measure.getName(), measure);
    }
  }

  @GuardedBy("monitor")
  private void emitDirectoryTable(
      /*@Nullable*/ TreeNode currentNode,
      /*@Nullable*/ String path,
      PrintWriter out,
      Formatter formatter) {
    out.write("<h2 style=\"margin-bottom:0;\">Views</h2>");
    if (currentNode == null) {
      formatter.format(
          "<p><font size=+2>Directory not found: %s. Return to root.</font></p>", path);
      currentNode = root;
    }
    if (currentNode == root || path == null) {
      path = "";
    }
    emitDirectoryHeader(path, out, formatter);
    out.write("<table class=\"title\" cellspacing=0 cellpadding=0>");
    for (Entry<String, TreeNode> entry : currentNode.children.entrySet()) {
      TreeNode child = entry.getValue();
      String relativePath = entry.getKey();
      if (child.viewName == null) { // Directory node, emit a row for directory.
        formatter.format(
            "<tr class=\"direct\"><td>Directory: <a href='?%s=%s'>%s</a> (%d %s)</td></tr>",
            QUERY_PATH,
            path + '/' + relativePath,
            relativePath,
            child.views,
            child.views > 1 ? "views" : "view");
      } else { // View node, emit a row for view.
        String viewName = child.viewName.asString();
        formatter.format(
            "<tr class=\"direct\"><td>View: <a href='?%s=%s'>%s</a></td></tr>",
            QUERY_PATH, path + '/' + relativePath, viewName);
      }
    }
    out.write("</table>");
    out.write("<p></p>");
  }

  // Searches the TreeNode whose absolute path matches the given path, started from root.
  // Returns null if such a TreeNode doesn't exist.
  @GuardedBy("monitor")
  private /*@Nullable*/ TreeNode findNode(/*@Nullable*/ String path) {
    if (Strings.isNullOrEmpty(path) || "/".equals(path)) { // Go back to the root directory.
      return root;
    } else {
      List<String> dirs = PATH_SPLITTER.splitToList(path);
      TreeNode node = root;
      for (int i = 0; i < dirs.size(); i++) {
        String dir = dirs.get(i);
        if ("".equals(dir) && i == 0) {
          continue; // Skip the first "", the path of root node.
        }
        if (!node.children.containsKey(dir)) {
          return null;
        } else {
          node = node.children.get(dir);
        }
      }
      return node;
    }
  }

  private static void emitDirectoryHeader(String path, PrintWriter out, Formatter formatter) {
    List<String> dirs = PATH_SPLITTER.splitToList(path);
    StringBuilder currentPath = new StringBuilder("");
    out.write("<h3>Current Path: ");
    for (int i = 0; i < dirs.size(); i++) {
      String dir = dirs.get(i);
      currentPath.append(dir);
      // create links to navigate back to parent directories.
      formatter.format("<a href='?%s=%s'>%s</a>", QUERY_PATH, currentPath.toString(), dir + '/');
      currentPath.append('/');
    }
    out.write("</h3>");
  }

  private static void emitViewData(
      /*@Nullable*/ ViewData viewData, View.Name viewName, PrintWriter out, Formatter formatter) {
    if (viewData == null) {
      formatter.format("<p class=\"view\">No Stats found for View %s.</p>", viewName.asString());
      return;
    }
    View view = viewData.getView();
    emitViewInfo(view, viewData.getWindowData(), out, formatter);
    formatter.format("<p class=\"view\">Stats for View: %s</p>", view.getName().asString());

    formatter.format("<table cellspacing=0 cellpadding=0>");
    emitViewDataTableHeader(view, out, formatter);
    for (Entry<List</*@Nullable*/ TagValue>, AggregationData> entry :
        viewData.getAggregationMap().entrySet()) {
      emitViewDataRow(view, entry, out, formatter);
    }
    out.write("</table>");
    out.write("<p></p>");
  }

  private static void emitViewInfo(
      View view, ViewData.AggregationWindowData windowData, PrintWriter out, Formatter formatter) {
    formatter.format("<table width=100%% cellspacing=0 cellpadding=0>");
    emitViewInfoHeader(out, formatter);

    out.write("<tbody>");
    out.write("<tr>"); // One row that represents the selected view.
    formatter.format("<td>%s</td>", view.getName().asString());
    formatter.format("<td class=\"borderLL\">%s</td>", view.getDescription());
    formatter.format("<td class=\"borderLL\">%s</td>", view.getMeasure().getName());
    String aggregationType =
        view.getAggregation()
            .match(
                Functions.returnConstant("Sum"),
                Functions.returnConstant("Count"),
                Functions.returnConstant("Distribution"),
                Functions.returnConstant("Last Value"),
                new Function<Aggregation, String>() {
                  @Override
                  public String apply(Aggregation arg) {
                    // TODO(songya): remove this once Mean aggregation is completely removed. Before
                    // that
                    // we need to continue supporting Mean, since it could still be used by users
                    // and some
                    // deprecated RPC views.
                    if (arg instanceof Aggregation.Mean) {
                      return "Mean";
                    }
                    throw new AssertionError();
                  }
                });
    formatter.format("<td class=\"borderLL\">%s</td>", aggregationType);
    windowData.match(
        new Function<ViewData.AggregationWindowData.CumulativeData, Void>() {
          @Override
          public Void apply(ViewData.AggregationWindowData.CumulativeData arg) {
            formatter.format("<td class=\"borderLL\">%s</td>", toDate(arg.getStart()));
            formatter.format("<td class=\"borderLL\">%s</td>", toDate(arg.getEnd()));
            return null;
          }
        },
        Functions.</*@Nullable*/ Void>throwAssertionError(), // No interval views will be displayed.
        Functions.</*@Nullable*/ Void>throwAssertionError());
    out.write("</tr>");
    out.write("</tbody>");
    out.write("</table>");
    out.write("<p></p>");
  }

  private static Date toDate(Timestamp timestamp) {
    return Date.from(
        Instant.ofEpochMilli(
            timestamp.getSeconds() * MILLIS_PER_SECOND
                + timestamp.getNanos() / NANOS_PER_MILLISECOND));
  }

  private static void emitViewInfoHeader(PrintWriter out, Formatter formatter) {
    out.write("<thead>");
    out.write("<tr>");
    formatter.format("<th colspan=1 align=left>%s</th>", TABLE_HEADER_VIEW);
    formatter.format("<th colspan=1 class=\"borderL\">%s</th>", TABLE_HEADER_DESCRIPTION);
    formatter.format("<th colspan=1 class=\"borderL\">%s</th>", TABLE_HEADER_MEASURE);
    formatter.format("<th colspan=1 class=\"borderL\">%s</th>", TABLE_HEADER_AGGREGATION);
    formatter.format("<th colspan=1 class=\"borderL\">%s</th>", TABLE_HEADER_START);
    formatter.format("<th colspan=1 class=\"borderL\">%s</th>", TABLE_HEADER_END);
    out.write("</tr>");
    out.write("</thead>");
  }

  private static void emitViewDataTableHeader(View view, PrintWriter out, Formatter formatter) {
    out.write("<thead>");
    out.write("<tr>");
    for (TagKey tagKey : view.getColumns()) {
      formatter.format("<th class=\"borderRL\">TagKey: %s (string)</th>", tagKey.getName());
    }
    String unit = view.getMeasure().getUnit();
    view.getAggregation()
        .match(
            new Function<Sum, Void>() {
              @Override
              public Void apply(Sum arg) {
                formatter.format("<th class=\"borderL\">%s, %s</th>", TABLE_HEADER_SUM, unit);
                return null;
              }
            },
            new Function<Count, Void>() {
              @Override
              public Void apply(Count arg) {
                formatter.format("<th class=\"borderL\">%s</th>", TABLE_HEADER_COUNT);
                return null;
              }
            },
            new Function<Distribution, Void>() {
              @Override
              public Void apply(Distribution arg) {
                formatter.format("<th>%s, %s</th>", TABLE_HEADER_MEAN, unit);
                formatter.format("<th class=\"borderL\">%s</th>", TABLE_HEADER_COUNT);
                formatter.format("<th class=\"borderL\">%s, %s</th>", TABLE_HEADER_MAX, unit);
                formatter.format("<th class=\"borderL\">%s, %s</th>", TABLE_HEADER_MIN, unit);
                formatter.format("<th class=\"borderL\">%s</th>", TABLE_HEADER_DEV);
                formatter.format("<th class=\"borderL\">%s</th>", TABLE_HEADER_HISTOGRAM);
                return null;
              }
            },
            new Function<LastValue, Void>() {
              @Override
              public Void apply(LastValue arg) {
                formatter.format(
                    "<th class=\"borderL\">%s, %s</th>", TABLE_HEADER_LAST_VALUE, unit);
                return null;
              }
            },
            new Function<Aggregation, Void>() {
              @Override
              public Void apply(Aggregation arg) {
                // TODO(songya): remove this once Mean aggregation is completely removed. Before
                // that
                // we need to continue supporting Mean, since it could still be used by users and
                // some
                // deprecated RPC views.
                if (arg instanceof Aggregation.Mean) {
                  formatter.format("<th>%s, %s</th>", TABLE_HEADER_MEAN, unit);
                  formatter.format("<th class=\"borderL\">%s</th>", TABLE_HEADER_COUNT);
                  return null;
                }
                throw new IllegalArgumentException("Unknown Aggregation.");
              }
            });
    out.write("</tr>");
    out.write("</thead>");
  }

  private static void emitViewDataRow(
      View view,
      Entry<List</*@Nullable*/ TagValue>, AggregationData> entry,
      PrintWriter out,
      Formatter formatter) {
    out.write("<tr>");
    for (/*@Nullable*/ TagValue tagValue : entry.getKey()) {
      String tagValueStr = tagValue == null ? "" : tagValue.asString();
      formatter.format("<td class=\"borderRL\">%s</td>", tagValueStr);
    }
    entry
        .getValue()
        .match(
            new Function<SumDataDouble, Void>() {
              @Override
              public Void apply(SumDataDouble arg) {
                formatter.format("<td class=\"borderLL\">%.3f</td>", arg.getSum());
                return null;
              }
            },
            new Function<SumDataLong, Void>() {
              @Override
              public Void apply(SumDataLong arg) {
                formatter.format("<td class=\"borderLL\">%d</td>", arg.getSum());
                return null;
              }
            },
            new Function<CountData, Void>() {
              @Override
              public Void apply(CountData arg) {
                formatter.format("<td class=\"borderLL\">%d</td>", arg.getCount());
                return null;
              }
            },
            new Function<DistributionData, Void>() {
              @Override
              public Void apply(DistributionData arg) {
                checkArgument(
                    view.getAggregation() instanceof Distribution, "Distribution expected.");
                formatter.format("<td>%.3f</td>", arg.getMean());
                formatter.format("<td class=\"borderLL\">%d</td>", arg.getCount());
                formatter.format("<td class=\"borderLL\">%.3f</td>", arg.getMax());
                formatter.format("<td class=\"borderLL\">%.3f</td>", arg.getMin());
                formatter.format(
                    "<td class=\"borderLL\">%.3f</td>", arg.getSumOfSquaredDeviations());
                emitHistogramBuckets(
                    ((Distribution) view.getAggregation()).getBucketBoundaries().getBoundaries(),
                    arg.getBucketCounts(),
                    out,
                    formatter);
                return null;
              }
            },
            new Function<LastValueDataDouble, Void>() {
              @Override
              public Void apply(LastValueDataDouble arg) {
                formatter.format("<td>%.3f</td>", arg.getLastValue());
                return null;
              }
            },
            new Function<LastValueDataLong, Void>() {
              @Override
              public Void apply(LastValueDataLong arg) {
                formatter.format("<td>%d</td>", arg.getLastValue());
                return null;
              }
            },
            new Function<AggregationData, Void>() {
              @Override
              public Void apply(AggregationData arg) {
                if (arg instanceof AggregationData.MeanData) {
                  AggregationData.MeanData meanData = (AggregationData.MeanData) arg;
                  formatter.format("<td>%.3f</td>", meanData.getMean());
                  formatter.format("<td class=\"borderLL\">%d</td>", meanData.getCount());
                  return null;
                }
                throw new IllegalArgumentException("Unknown Aggregation.");
              }
            });
    out.write("</tr>");
  }

  private static void emitHistogramBuckets(
      List<Double> bucketBoundaries,
      List<Long> bucketCounts,
      PrintWriter out,
      Formatter formatter) {
    checkArgument(
        bucketBoundaries.size() == bucketCounts.size() - 1,
        "Bucket boundaries and counts don't match");
    out.write("<td class=\"borderLL\">");
    out.write("<table>");
    formatter.format(
        "<thead><tr><th>%s</th><th>%s</th></tr></thead>",
        TABLE_HEADER_RANGE, TABLE_HEADER_BUCKET_SIZE);
    out.write("<tbody>");
    for (int i = 0; i < bucketCounts.size(); i++) {
      double low = i == 0 ? Double.NEGATIVE_INFINITY : bucketBoundaries.get(i - 1);
      double high =
          i == bucketCounts.size() - 1 ? Double.POSITIVE_INFINITY : bucketBoundaries.get(i);
      out.write("<tr>");
      formatter.format("<td>[%.3f...%.3f)</td>", low, high);
      formatter.format("<td>%d</td>", bucketCounts.get(i));
      out.write("</tr>");
    }
    out.write("</tbody>");
    out.write("</table>");
    out.write("</td>");
  }

  private static void emitMeasureTable(
      Map<String, Measure> measures, PrintWriter out, Formatter formatter) {
    out.write("<h2>Measures with Views</h2>");
    out.write("<p>Below are the measures used in registered views.</p>");
    out.write("<p></p>");
    formatter.format("<table cellspacing=0 cellpadding=0>");
    emitMeasureTableHeader(out, formatter);
    out.write("<tbody>");
    for (Entry<String, Measure> entry : measures.entrySet()) {
      emitMeasureTableRow(entry.getValue(), out, formatter);
    }
    out.write("</tbody>");
    out.write("</table>");
    out.write("<p></p>");
  }

  private static void emitMeasureTableHeader(PrintWriter out, Formatter formatter) {
    out.write("<thead>");
    out.write("<tr>");
    formatter.format("<th colspan=1>%s</th>", TABLE_HEADER_MEASURE);
    formatter.format("<th colspan=1 class=\"borderL\">%s</th>", TABLE_HEADER_DESCRIPTION);
    formatter.format("<th colspan=1 class=\"borderL\">%s</th>", TABLE_HEADER_UNIT);
    formatter.format("<th colspan=1 class=\"borderL\">%s</th>", TABLE_HEADER_MEASURE_TYPE);
    out.write("</tr>");
    out.write("</thead>");
  }

  private static void emitMeasureTableRow(Measure measure, PrintWriter out, Formatter formatter) {
    out.write("<tr>");
    formatter.format("<td><b>%s</b></td>", measure.getName());
    formatter.format("<td class=\"borderLL\">%s&nbsp;</td>", measure.getDescription());
    formatter.format("<td class=\"borderLL\">%s&nbsp;</td>", measure.getUnit());
    String measureType =
        measure.match(
            Functions.returnConstant("Double"),
            Functions.returnConstant("Long"),
            Functions.throwAssertionError());
    formatter.format("<td class=\"borderLL\">%s&nbsp;</td>", measureType);
    out.write("</tr>");
  }

  static StatszZPageHandler create(ViewManager viewManager) {
    return new StatszZPageHandler(viewManager);
  }

  private StatszZPageHandler(ViewManager viewManager) {
    this.viewManager = viewManager;
  }

  /*
   * TreeNode for storing the structure of views and directories that they're in. Think of this as
   * file descriptors for view: non-leaf nodes are directories which may contain views or other
   * directories, and leaf nodes are the ones with actual information on views. Each non-leaf node
   * also has the number of views under its directory.
   */
  private static class TreeNode {
    // Only leaf nodes have views.
    @javax.annotation.Nullable final View.Name viewName;

    // A mapping from relative path to children TreeNodes. Sorted by the relative path.
    SortedMap<String, TreeNode> children = Maps.newTreeMap();

    // The number of views that a directory contains. 0 for leaf node.
    int views = 0;

    TreeNode() {
      this.viewName = null;
    }

    TreeNode(View.Name viewName) {
      this.viewName = checkNotNull(viewName, "view name");
    }
  }
}
