/**
 *
 * 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.hadoop.hbase;

import edu.umd.cs.findbugs.annotations.Nullable;
import java.util.Collections;
import java.util.EnumSet;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.stream.Collectors;

import org.apache.hadoop.hbase.client.RegionStatesCount;
import org.apache.hadoop.hbase.master.RegionState;
import org.apache.yetus.audience.InterfaceAudience;

import org.apache.hbase.thirdparty.com.google.common.base.Preconditions;
import org.apache.hbase.thirdparty.com.google.protobuf.UnsafeByteOperations;
import org.apache.hadoop.hbase.shaded.protobuf.ProtobufUtil;
import org.apache.hadoop.hbase.shaded.protobuf.generated.ClusterStatusProtos;
import org.apache.hadoop.hbase.shaded.protobuf.generated.ClusterStatusProtos.Option;
import org.apache.hadoop.hbase.shaded.protobuf.generated.FSProtos;
import org.apache.hadoop.hbase.shaded.protobuf.generated.HBaseProtos;

@InterfaceAudience.Private
public final class ClusterMetricsBuilder {

  public static ClusterStatusProtos.ClusterStatus toClusterStatus(ClusterMetrics metrics) {
    ClusterStatusProtos.ClusterStatus.Builder builder
        = ClusterStatusProtos.ClusterStatus.newBuilder()
        .addAllBackupMasters(metrics.getBackupMasterNames().stream()
            .map(ProtobufUtil::toServerName).collect(Collectors.toList()))
        .addAllDeadServers(metrics.getDeadServerNames().stream()
            .map(ProtobufUtil::toServerName).collect(Collectors.toList()))
        .addAllLiveServers(metrics.getLiveServerMetrics().entrySet().stream()
            .map(s -> ClusterStatusProtos.LiveServerInfo
                .newBuilder()
                .setServer(ProtobufUtil.toServerName(s.getKey()))
                .setServerLoad(ServerMetricsBuilder.toServerLoad(s.getValue()))
                .build())
            .collect(Collectors.toList()))
        .addAllMasterCoprocessors(metrics.getMasterCoprocessorNames().stream()
            .map(n -> HBaseProtos.Coprocessor.newBuilder().setName(n).build())
            .collect(Collectors.toList()))
        .addAllRegionsInTransition(metrics.getRegionStatesInTransition().stream()
            .map(r -> ClusterStatusProtos.RegionInTransition
                .newBuilder()
                .setSpec(HBaseProtos.RegionSpecifier
                    .newBuilder()
                    .setType(HBaseProtos.RegionSpecifier.RegionSpecifierType.REGION_NAME)
                    .setValue(UnsafeByteOperations.unsafeWrap(r.getRegion().getRegionName()))
                    .build())
                .setRegionState(r.convert())
                .build())
            .collect(Collectors.toList()))
        .setMasterInfoPort(metrics.getMasterInfoPort())
        .addAllServersName(metrics.getServersName().stream().map(ProtobufUtil::toServerName)
          .collect(Collectors.toList()))
        .addAllTableRegionStatesCount(metrics.getTableRegionStatesCount().entrySet().stream()
          .map(status ->
            ClusterStatusProtos.TableRegionStatesCount.newBuilder()
              .setTableName(ProtobufUtil.toProtoTableName((status.getKey())))
              .setRegionStatesCount(ProtobufUtil.toTableRegionStatesCount(status.getValue()))
              .build())
          .collect(Collectors.toList()));
    if (metrics.getMasterName() != null) {
      builder.setMaster(ProtobufUtil.toServerName((metrics.getMasterName())));
    }
    if (metrics.getBalancerOn() != null) {
      builder.setBalancerOn(metrics.getBalancerOn());
    }
    if (metrics.getClusterId() != null) {
      builder.setClusterId(new ClusterId(metrics.getClusterId()).convert());
    }
    if (metrics.getHBaseVersion() != null) {
      builder.setHbaseVersion(
          FSProtos.HBaseVersionFileContent.newBuilder()
              .setVersion(metrics.getHBaseVersion()));
    }
    return builder.build();
  }

  public static ClusterMetrics toClusterMetrics(
      ClusterStatusProtos.ClusterStatus proto) {
    ClusterMetricsBuilder builder = ClusterMetricsBuilder.newBuilder();
    builder.setLiveServerMetrics(proto.getLiveServersList().stream()
        .collect(Collectors.toMap(e -> ProtobufUtil.toServerName(e.getServer()),
            ServerMetricsBuilder::toServerMetrics)))
        .setDeadServerNames(proto.getDeadServersList().stream()
            .map(ProtobufUtil::toServerName)
            .collect(Collectors.toList()))
        .setBackerMasterNames(proto.getBackupMastersList().stream()
            .map(ProtobufUtil::toServerName)
            .collect(Collectors.toList()))
        .setRegionsInTransition(proto.getRegionsInTransitionList().stream()
            .map(ClusterStatusProtos.RegionInTransition::getRegionState)
            .map(RegionState::convert)
            .collect(Collectors.toList()))
        .setMasterCoprocessorNames(proto.getMasterCoprocessorsList().stream()
            .map(HBaseProtos.Coprocessor::getName)
            .collect(Collectors.toList()))
        .setServerNames(proto.getServersNameList().stream().map(ProtobufUtil::toServerName)
            .collect(Collectors.toList()))
        .setTableRegionStatesCount(
          proto.getTableRegionStatesCountList().stream()
          .collect(Collectors.toMap(
            e -> ProtobufUtil.toTableName(e.getTableName()),
            e -> ProtobufUtil.toTableRegionStatesCount(e.getRegionStatesCount()))));
    if (proto.hasClusterId()) {
      builder.setClusterId(ClusterId.convert(proto.getClusterId()).toString());
    }

    if (proto.hasHbaseVersion()) {
      builder.setHBaseVersion(proto.getHbaseVersion().getVersion());
    }

    if (proto.hasMaster()) {
      builder.setMasterName(ProtobufUtil.toServerName(proto.getMaster()));
    }

    if (proto.hasBalancerOn()) {
      builder.setBalancerOn(proto.getBalancerOn());
    }

    if (proto.hasMasterInfoPort()) {
      builder.setMasterInfoPort(proto.getMasterInfoPort());
    }
    return builder.build();
  }

  /**
   * Convert ClusterStatusProtos.Option to ClusterMetrics.Option
   * @param option a ClusterStatusProtos.Option
   * @return converted ClusterMetrics.Option
   */
  public static ClusterMetrics.Option toOption(ClusterStatusProtos.Option option) {
    switch (option) {
      case HBASE_VERSION: return ClusterMetrics.Option.HBASE_VERSION;
      case LIVE_SERVERS: return ClusterMetrics.Option.LIVE_SERVERS;
      case DEAD_SERVERS: return ClusterMetrics.Option.DEAD_SERVERS;
      case REGIONS_IN_TRANSITION: return ClusterMetrics.Option.REGIONS_IN_TRANSITION;
      case CLUSTER_ID: return ClusterMetrics.Option.CLUSTER_ID;
      case MASTER_COPROCESSORS: return ClusterMetrics.Option.MASTER_COPROCESSORS;
      case MASTER: return ClusterMetrics.Option.MASTER;
      case BACKUP_MASTERS: return ClusterMetrics.Option.BACKUP_MASTERS;
      case BALANCER_ON: return ClusterMetrics.Option.BALANCER_ON;
      case SERVERS_NAME: return ClusterMetrics.Option.SERVERS_NAME;
      case MASTER_INFO_PORT: return ClusterMetrics.Option.MASTER_INFO_PORT;
      case TABLE_TO_REGIONS_COUNT: return ClusterMetrics.Option.TABLE_TO_REGIONS_COUNT;
      // should not reach here
      default: throw new IllegalArgumentException("Invalid option: " + option);
    }
  }

  /**
   * Convert ClusterMetrics.Option to ClusterStatusProtos.Option
   * @param option a ClusterMetrics.Option
   * @return converted ClusterStatusProtos.Option
   */
  public static ClusterStatusProtos.Option toOption(ClusterMetrics.Option option) {
    switch (option) {
      case HBASE_VERSION: return ClusterStatusProtos.Option.HBASE_VERSION;
      case LIVE_SERVERS: return ClusterStatusProtos.Option.LIVE_SERVERS;
      case DEAD_SERVERS: return ClusterStatusProtos.Option.DEAD_SERVERS;
      case REGIONS_IN_TRANSITION: return ClusterStatusProtos.Option.REGIONS_IN_TRANSITION;
      case CLUSTER_ID: return ClusterStatusProtos.Option.CLUSTER_ID;
      case MASTER_COPROCESSORS: return ClusterStatusProtos.Option.MASTER_COPROCESSORS;
      case MASTER: return ClusterStatusProtos.Option.MASTER;
      case BACKUP_MASTERS: return ClusterStatusProtos.Option.BACKUP_MASTERS;
      case BALANCER_ON: return ClusterStatusProtos.Option.BALANCER_ON;
      case SERVERS_NAME: return Option.SERVERS_NAME;
      case MASTER_INFO_PORT: return ClusterStatusProtos.Option.MASTER_INFO_PORT;
      case TABLE_TO_REGIONS_COUNT: return ClusterStatusProtos.Option.TABLE_TO_REGIONS_COUNT;
      // should not reach here
      default: throw new IllegalArgumentException("Invalid option: " + option);
    }
  }

  /**
   * Convert a list of ClusterStatusProtos.Option to an enum set of ClusterMetrics.Option
   * @param options the pb options
   * @return an enum set of ClusterMetrics.Option
   */
  public static EnumSet<ClusterMetrics.Option> toOptions(List<ClusterStatusProtos.Option> options) {
    return options.stream().map(ClusterMetricsBuilder::toOption)
        .collect(Collectors.toCollection(() -> EnumSet.noneOf(ClusterMetrics.Option.class)));
  }

  /**
   * Convert an enum set of ClusterMetrics.Option to a list of ClusterStatusProtos.Option
   * @param options the ClusterMetrics options
   * @return a list of ClusterStatusProtos.Option
   */
  public static List<ClusterStatusProtos.Option> toOptions(EnumSet<ClusterMetrics.Option> options) {
    return options.stream().map(ClusterMetricsBuilder::toOption).collect(Collectors.toList());
  }

  public static ClusterMetricsBuilder newBuilder() {
    return new ClusterMetricsBuilder();
  }
  @Nullable
  private String hbaseVersion;
  private List<ServerName> deadServerNames = Collections.emptyList();
  private Map<ServerName, ServerMetrics> liveServerMetrics = new TreeMap<>();
  @Nullable
  private ServerName masterName;
  private List<ServerName> backupMasterNames = Collections.emptyList();
  private List<RegionState> regionsInTransition = Collections.emptyList();
  @Nullable
  private String clusterId;
  private List<String> masterCoprocessorNames = Collections.emptyList();
  @Nullable
  private Boolean balancerOn;
  private int masterInfoPort;
  private List<ServerName> serversName = Collections.emptyList();
  private Map<TableName, RegionStatesCount> tableRegionStatesCount = Collections.emptyMap();

  private ClusterMetricsBuilder() {
  }
  public ClusterMetricsBuilder setHBaseVersion(String value) {
    this.hbaseVersion = value;
    return this;
  }
  public ClusterMetricsBuilder setDeadServerNames(List<ServerName> value) {
    this.deadServerNames = value;
    return this;
  }

  public ClusterMetricsBuilder setLiveServerMetrics(Map<ServerName, ServerMetrics> value) {
    liveServerMetrics.putAll(value);
    return this;
  }

  public ClusterMetricsBuilder setMasterName(ServerName value) {
    this.masterName = value;
    return this;
  }
  public ClusterMetricsBuilder setBackerMasterNames(List<ServerName> value) {
    this.backupMasterNames = value;
    return this;
  }
  public ClusterMetricsBuilder setRegionsInTransition(List<RegionState> value) {
    this.regionsInTransition = value;
    return this;
  }
  public ClusterMetricsBuilder setClusterId(String value) {
    this.clusterId = value;
    return this;
  }
  public ClusterMetricsBuilder setMasterCoprocessorNames(List<String> value) {
    this.masterCoprocessorNames = value;
    return this;
  }
  public ClusterMetricsBuilder setBalancerOn(@Nullable Boolean value) {
    this.balancerOn = value;
    return this;
  }
  public ClusterMetricsBuilder setMasterInfoPort(int value) {
    this.masterInfoPort = value;
    return this;
  }
  public ClusterMetricsBuilder setServerNames(List<ServerName> serversName) {
    this.serversName = serversName;
    return this;
  }

  public ClusterMetricsBuilder setTableRegionStatesCount(
      Map<TableName, RegionStatesCount> tableRegionStatesCount) {
    this.tableRegionStatesCount = tableRegionStatesCount;
    return this;
  }

  public ClusterMetrics build() {
    return new ClusterMetricsImpl(
        hbaseVersion,
        deadServerNames,
        liveServerMetrics,
        masterName,
        backupMasterNames,
        regionsInTransition,
        clusterId,
        masterCoprocessorNames,
        balancerOn,
        masterInfoPort,
        serversName,
        tableRegionStatesCount
    );
  }
  private static class ClusterMetricsImpl implements ClusterMetrics {
    @Nullable
    private final String hbaseVersion;
    private final List<ServerName> deadServerNames;
    private final Map<ServerName, ServerMetrics> liveServerMetrics;
    @Nullable
    private final ServerName masterName;
    private final List<ServerName> backupMasterNames;
    private final List<RegionState> regionsInTransition;
    @Nullable
    private final String clusterId;
    private final List<String> masterCoprocessorNames;
    @Nullable
    private final Boolean balancerOn;
    private final int masterInfoPort;
    private final List<ServerName> serversName;
    private final Map<TableName, RegionStatesCount> tableRegionStatesCount;

    ClusterMetricsImpl(String hbaseVersion, List<ServerName> deadServerNames,
        Map<ServerName, ServerMetrics> liveServerMetrics,
        ServerName masterName,
        List<ServerName> backupMasterNames,
        List<RegionState> regionsInTransition,
        String clusterId,
        List<String> masterCoprocessorNames,
        Boolean balancerOn,
        int masterInfoPort,
        List<ServerName> serversName,
        Map<TableName, RegionStatesCount> tableRegionStatesCount) {
      this.hbaseVersion = hbaseVersion;
      this.deadServerNames = Preconditions.checkNotNull(deadServerNames);
      this.liveServerMetrics = Preconditions.checkNotNull(liveServerMetrics);
      this.masterName = masterName;
      this.backupMasterNames = Preconditions.checkNotNull(backupMasterNames);
      this.regionsInTransition = Preconditions.checkNotNull(regionsInTransition);
      this.clusterId = clusterId;
      this.masterCoprocessorNames = Preconditions.checkNotNull(masterCoprocessorNames);
      this.balancerOn = balancerOn;
      this.masterInfoPort = masterInfoPort;
      this.serversName = serversName;
      this.tableRegionStatesCount = Preconditions.checkNotNull(tableRegionStatesCount);
    }

    @Override
    public String getHBaseVersion() {
      return hbaseVersion;
    }

    @Override
    public List<ServerName> getDeadServerNames() {
      return Collections.unmodifiableList(deadServerNames);
    }

    @Override
    public Map<ServerName, ServerMetrics> getLiveServerMetrics() {
      return Collections.unmodifiableMap(liveServerMetrics);
    }

    @Override
    public ServerName getMasterName() {
      return masterName;
    }

    @Override
    public List<ServerName> getBackupMasterNames() {
      return Collections.unmodifiableList(backupMasterNames);
    }

    @Override
    public List<RegionState> getRegionStatesInTransition() {
      return Collections.unmodifiableList(regionsInTransition);
    }

    @Override
    public String getClusterId() {
      return clusterId;
    }

    @Override
    public List<String> getMasterCoprocessorNames() {
      return Collections.unmodifiableList(masterCoprocessorNames);
    }

    @Override
    public Boolean getBalancerOn() {
      return balancerOn;
    }

    @Override
    public int getMasterInfoPort() {
      return masterInfoPort;
    }

    @Override
    public List<ServerName> getServersName() {
      return Collections.unmodifiableList(serversName);
    }

    @Override
    public Map<TableName, RegionStatesCount> getTableRegionStatesCount() {
      return Collections.unmodifiableMap(tableRegionStatesCount);
    }

    @Override
    public String toString() {
      StringBuilder sb = new StringBuilder(1024);
      sb.append("Master: " + getMasterName());

      int backupMastersSize = getBackupMasterNames().size();
      sb.append("\nNumber of backup masters: " + backupMastersSize);
      if (backupMastersSize > 0) {
        for (ServerName serverName: getBackupMasterNames()) {
          sb.append("\n  " + serverName);
        }
      }

      int serversSize = getLiveServerMetrics().size();
      int serversNameSize = getServersName().size();
      sb.append("\nNumber of live region servers: "
          + (serversSize > 0 ? serversSize : serversNameSize));
      if (serversSize > 0) {
        for (ServerName serverName : getLiveServerMetrics().keySet()) {
          sb.append("\n  " + serverName.getServerName());
        }
      } else if (serversNameSize > 0) {
        for (ServerName serverName : getServersName()) {
          sb.append("\n  " + serverName.getServerName());
        }
      }

      int deadServerSize = getDeadServerNames().size();
      sb.append("\nNumber of dead region servers: " + deadServerSize);
      if (deadServerSize > 0) {
        for (ServerName serverName : getDeadServerNames()) {
          sb.append("\n  " + serverName);
        }
      }

      sb.append("\nAverage load: " + getAverageLoad());
      sb.append("\nNumber of requests: " + getRequestCount());
      sb.append("\nNumber of regions: " + getRegionCount());

      int ritSize = getRegionStatesInTransition().size();
      sb.append("\nNumber of regions in transition: " + ritSize);
      if (ritSize > 0) {
        for (RegionState state : getRegionStatesInTransition()) {
          sb.append("\n  " + state.toDescriptiveString());
        }
      }
      return sb.toString();
    }
  }
}
