content: /*
   * Licensed to the Apache Software Foundation (ASF) under one
   * or more contributor license agreements.  See the NOTICE file
   * distributed with this work for additional information
   * regarding copyright ownership.  The ASF licenses this file
   * to you 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 org.apache.cassandra.tools;
  
  import java.io.IOException;
  import java.io.PrintStream;
  import java.util.Iterator;
  import java.util.List;
  import java.util.Map;
  import java.util.Map.Entry;
  import java.util.Spliterator;
  import java.util.Spliterators;
  import java.util.TreeMap;
  import java.util.function.LongFunction;
  import java.util.stream.Stream;
  import java.util.stream.StreamSupport;
  
  import com.google.common.base.Strings;
  import com.google.common.collect.Lists;
  
  import org.apache.cassandra.config.DatabaseDescriptor;
  import org.apache.cassandra.cql3.ColumnIdentifier;
  import org.apache.cassandra.db.SerializationHeader;
  import org.apache.cassandra.db.marshal.UTF8Type;
  import org.apache.cassandra.dht.IPartitioner;
  import org.apache.cassandra.exceptions.ConfigurationException;
  import org.apache.cassandra.index.SecondaryIndexManager;
  import org.apache.cassandra.io.sstable.Descriptor;
  import org.apache.cassandra.io.sstable.format.StatsComponent;
  import org.apache.cassandra.io.sstable.metadata.MetadataType;
  import org.apache.cassandra.schema.IndexMetadata;
  import org.apache.cassandra.schema.Indexes;
  import org.apache.cassandra.schema.TableMetadata;
  import org.apache.cassandra.utils.EstimatedHistogram;
  import org.apache.cassandra.utils.FBUtilities;
  import org.apache.cassandra.utils.streamhist.TombstoneHistogram;
  
  import static java.lang.String.format;
  
  @SuppressWarnings("serial")
  public final class Util
  {
      static final String RESET = "\u001B[0m";
      static final String BLUE = "\u001B[34m";
      static final String CYAN = "\u001B[36m";
      static final String WHITE = "\u001B[37m";
      private static final List<String> ANSI_COLORS = Lists.newArrayList(RESET, BLUE, CYAN, WHITE);
  
      private static final String FULL_BAR_UNICODE = Strings.repeat("\u2593", 30);
      private static final String EMPTY_BAR_UNICODE = Strings.repeat("\u2591", 30);
      private static final String FULL_BAR_ASCII = Strings.repeat("#", 30);
      private static final String EMPTY_BAR_ASCII = Strings.repeat("-", 30);
  
      private static final TreeMap<Double, String> BARS_UNICODE = new TreeMap<Double, String>()
      {{
          this.put(1.0,       "\u2589"); // full, actually using 7/8th for bad font impls of fullblock
          this.put(7.0 / 8.0, "\u2589"); // 7/8ths left block
          this.put(3.0 / 4.0, "\u258A"); // 3/4th block
          this.put(5.0 / 8.0, "\u258B"); // 5/8th
          this.put(3.0 / 8.0, "\u258D"); // three eighths, skips 1/2 due to font inconsistencies
          this.put(1.0 / 4.0, "\u258E"); // 1/4th
          this.put(1.0 / 8.0, "\u258F"); // 1/8th
      }};
  
      private static final TreeMap<Double, String> BARS_ASCII = new TreeMap<Double, String>()
      {{
          this.put(1.00, "O");
          this.put(0.75, "o");
          this.put(0.30, ".");
      }};
  
      private static TreeMap<Double, String> barmap(boolean unicode)
      {
          return unicode ? BARS_UNICODE : BARS_ASCII;
      }
  
      public static String progress(double percentComplete, int width, boolean unicode)
      {
          assert percentComplete >= 0 && percentComplete <= 1;
          int cols = (int) (percentComplete * width);
          return (unicode ? FULL_BAR_UNICODE : FULL_BAR_ASCII).substring(width - cols) +
                 (unicode ? EMPTY_BAR_UNICODE : EMPTY_BAR_ASCII ).substring(cols);
      }
  
      public static String stripANSI(String string)
      {
          return ANSI_COLORS.stream().reduce(string, (a, b) -> a.replace(b, ""));
      }
  
      public static int countANSI(String string)
      {
          return string.length() - stripANSI(string).length();
      }
  
      public static String wrapQuiet(String toWrap, boolean color)
      {
          if (Strings.isNullOrEmpty(toWrap))
          {
              return "";
          }
          StringBuilder sb = new StringBuilder();
          if (color) sb.append(WHITE);
          sb.append("(");
          sb.append(toWrap);
          sb.append(")");
          if (color) sb.append(RESET);
          return sb.toString();
      }
  
      public static class TermHistogram
      {
          public long max;
          public long min;
          public double sum;
          int maxCountLength = 5;
          int maxOffsetLength = 5;
          Map<? extends Number, Long> histogram;
          LongFunction<String> offsetName;
          LongFunction<String> countName;
          String title;
  
          public TermHistogram(Map<? extends Number, Long> histogram,
                  String title,
                  LongFunction<String> offsetName,
                  LongFunction<String> countName)
          {
              this.offsetName = offsetName;
              this.countName = countName;
              this.histogram = histogram;
              this.title = title;
              maxOffsetLength = title.length();
              histogram.entrySet().stream().forEach(e ->
              {
                  max = Math.max(max, e.getValue());
                  min = Math.min(min, e.getValue());
                  sum += e.getValue();
                  // find max width, but remove ansi sequences first
                  maxCountLength = Math.max(maxCountLength, stripANSI(countName.apply(e.getValue())).length());
                  maxOffsetLength = Math.max(maxOffsetLength, stripANSI(offsetName.apply(e.getKey().longValue())).length());
              });
          }
  
          public TermHistogram(TombstoneHistogram histogram,
                  String title,
                  LongFunction<String> offsetName,
                  LongFunction<String> countName)
          {
              this(new TreeMap<Number, Long>()
              {
                  {
                      histogram.forEach((point, value) -> {
                          this.put(point, (long) value);
                      });
                  }
              }, title, offsetName, countName);
          }
  
          public TermHistogram(EstimatedHistogram histogram,
                  String title,
                  LongFunction<String> offsetName,
                  LongFunction<String> countName)
          {
              this(new TreeMap<Number, Long>()
              {
                  {
                      long[] counts = histogram.getBuckets(false);
                      long[] offsets = histogram.getBucketOffsets();
                      for (int i = 0; i < counts.length; i++)
                      {
                          long e = counts[i];
                          if (e > 0)
                          {
                              put(offsets[i], e);
                          }
                      }
                  }
              }, title, offsetName, countName);
          }
  
          public String bar(long count, int length, String color, boolean unicode)
          {
              if (color == null) color = "";
              StringBuilder sb = new StringBuilder(color);
              long barVal = count;
              int intWidth = (int) (barVal * 1.0 / max * length);
              double remainderWidth = (barVal * 1.0 / max * length) - intWidth;
              sb.append(Strings.repeat(barmap(unicode).get(1.0), intWidth));
  
              if (barmap(unicode).floorKey(remainderWidth) != null)
                  sb.append(barmap(unicode).get(barmap(unicode).floorKey(remainderWidth)));
  
              if(!Strings.isNullOrEmpty(color))
                  sb.append(RESET);
  
              return sb.toString();
          }
  
          public void printHistogram(PrintStream out, boolean color, boolean unicode)
          {
              // String.format includes ansi sequences in the count, so need to modify the lengths
              int offsetTitleLength = color ? maxOffsetLength + BLUE.length() : maxOffsetLength;
              out.printf("   %-" + offsetTitleLength + "s %s %-" + maxCountLength + "s  %s  %sHistogram%s %n",
                         color ? BLUE + title : title,
                         color ? CYAN + "|" + BLUE : "|",
                         "Count",
                         wrapQuiet("%", color),
                         color ? BLUE : "",
                         color ? RESET : "");
              histogram.entrySet().stream().forEach(e ->
              {
                  String offset = offsetName.apply(e.getKey().longValue());
                  long count = e.getValue();
                  String histo = bar(count, 30, color? WHITE : null, unicode);
                  int mol = color ? maxOffsetLength + countANSI(offset) : maxOffsetLength;
                  int mcl = color ? maxCountLength + countANSI(countName.apply(count)) : maxCountLength;
                  out.printf("   %-" + mol + "s %s %" + mcl + "s %s %s%n",
                             offset,
                             color ? CYAN + "|" + RESET : "|",
                             countName.apply(count),
                             wrapQuiet(String.format("%3s", (int) (100 * ((double) count / sum))), color),
                             histo);
              });
              EstimatedHistogram eh = new EstimatedHistogram(165);
              for (Entry<? extends Number, Long> e : histogram.entrySet())
              {
                  eh.add(e.getKey().longValue(), e.getValue());
              }
              String[] percentiles = new String[]{"50th", "75th", "95th", "98th", "99th", "Min", "Max"};
              long[] data = new long[]
              {
                  eh.percentile(.5),
                  eh.percentile(.75),
                  eh.percentile(.95),
                  eh.percentile(.98),
                  eh.percentile(.99),
                  eh.min(),
                  eh.max(),
              };
              out.println((color ? BLUE : "") + "   Percentiles" + (color ? RESET : ""));
  
              for (int i = 0; i < percentiles.length; i++)
              {
                  out.println(format("   %s%-10s%s%s",
                                     (color ? BLUE : ""),
                                     percentiles[i],
                                     (color ? RESET : ""),
                                     offsetName.apply(data[i])));
              }
          }
      }
      private Util()
      {
      }
  
      /**
       * This is used by standalone tools to force static initialization of DatabaseDescriptor, and fail if configuration
       * is bad.
       */
      public static void initDatabaseDescriptor()
      {
          try
          {
              DatabaseDescriptor.toolInitialization();
          }
          catch (Throwable e)
          {
              boolean logStackTrace = !(e instanceof ConfigurationException) || ((ConfigurationException) e).logStackTrace;
              System.out.println("Exception (" + e.getClass().getName() + ") encountered during startup: " + e.getMessage());
  
              if (logStackTrace)
              {
                  e.printStackTrace();
                  System.exit(3);
              }
              else
              {
                  System.err.println(e.getMessage());
                  System.exit(3);
              }
          }
      }
  
      public static <T> Stream<T> iterToStream(Iterator<T> iter)
      {
          Spliterator<T> splititer = Spliterators.spliteratorUnknownSize(iter, Spliterator.IMMUTABLE);
          return StreamSupport.stream(splititer, false);
      }
  
      /**
       * Construct table schema from info stored in SSTable's Stats.db
       *
       * @param desc SSTable's descriptor
       * @return Restored CFMetaData
       * @throws IOException when Stats.db cannot be read
       */
      public static TableMetadata metadataFromSSTable(Descriptor desc) throws IOException
      {
          if (!desc.version.isCompatible())
              throw new IOException("Unsupported SSTable version " + desc.getFormat().name() + "/" + desc.version);
  
          StatsComponent statsComponent = StatsComponent.load(desc, MetadataType.STATS, MetadataType.HEADER);
          SerializationHeader.Component header = statsComponent.serializationHeader();
  
          IPartitioner partitioner = FBUtilities.newPartitioner(desc);
  
          TableMetadata.Builder builder = TableMetadata.builder("keyspace", "table").partitioner(partitioner).offline();
          header.getStaticColumns().entrySet().stream()
                  .forEach(entry -> {
                      ColumnIdentifier ident = ColumnIdentifier.getInterned(UTF8Type.instance.getString(entry.getKey()), true);
                      builder.addStaticColumn(ident, entry.getValue());
                  });
          header.getRegularColumns().entrySet().stream()
                  .forEach(entry -> {
                      ColumnIdentifier ident = ColumnIdentifier.getInterned(UTF8Type.instance.getString(entry.getKey()), true);
                      builder.addRegularColumn(ident, entry.getValue());
                  });
          builder.addPartitionKeyColumn("PartitionKey", header.getKeyType());
          for (int i = 0; i < header.getClusteringTypes().size(); i++)
          {
              builder.addClusteringColumn("clustering" + (i > 0 ? i : ""), header.getClusteringTypes().get(i));
          }
          if (SecondaryIndexManager.isIndexColumnFamily(desc.cfname))
          {
              String index = SecondaryIndexManager.getIndexName(desc.cfname);
              // Just set the Kind of index to CUSTOM, which is an irrelevant parameter that doesn't make any effect on the result
              IndexMetadata indexMetadata = IndexMetadata.fromSchemaMetadata(index, IndexMetadata.Kind.CUSTOM, null);
              Indexes indexes = Indexes.of(indexMetadata);
              builder.indexes(indexes);
              builder.kind(TableMetadata.Kind.INDEX);
          }
          return builder.build();
      }
  }