/*
 * Copyright The OpenTelemetry Authors
 * SPDX-License-Identifier: Apache-2.0
 */

package io.opentelemetry.exporter.otlp.internal;

import static io.opentelemetry.proto.metrics.v1.AggregationTemporality.AGGREGATION_TEMPORALITY_CUMULATIVE;
import static io.opentelemetry.proto.metrics.v1.AggregationTemporality.AGGREGATION_TEMPORALITY_DELTA;
import static io.opentelemetry.proto.metrics.v1.AggregationTemporality.AGGREGATION_TEMPORALITY_UNSPECIFIED;

import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.proto.common.v1.KeyValue;
import io.opentelemetry.proto.metrics.v1.AggregationTemporality;
import io.opentelemetry.proto.metrics.v1.Gauge;
import io.opentelemetry.proto.metrics.v1.Histogram;
import io.opentelemetry.proto.metrics.v1.HistogramDataPoint;
import io.opentelemetry.proto.metrics.v1.InstrumentationLibraryMetrics;
import io.opentelemetry.proto.metrics.v1.Metric;
import io.opentelemetry.proto.metrics.v1.NumberDataPoint;
import io.opentelemetry.proto.metrics.v1.ResourceMetrics;
import io.opentelemetry.proto.metrics.v1.Sum;
import io.opentelemetry.proto.metrics.v1.Summary;
import io.opentelemetry.proto.metrics.v1.SummaryDataPoint;
import io.opentelemetry.sdk.common.InstrumentationLibraryInfo;
import io.opentelemetry.sdk.metrics.data.DoubleGaugeData;
import io.opentelemetry.sdk.metrics.data.DoubleHistogramData;
import io.opentelemetry.sdk.metrics.data.DoubleHistogramPointData;
import io.opentelemetry.sdk.metrics.data.DoublePointData;
import io.opentelemetry.sdk.metrics.data.DoubleSumData;
import io.opentelemetry.sdk.metrics.data.DoubleSummaryData;
import io.opentelemetry.sdk.metrics.data.DoubleSummaryPointData;
import io.opentelemetry.sdk.metrics.data.LongGaugeData;
import io.opentelemetry.sdk.metrics.data.LongPointData;
import io.opentelemetry.sdk.metrics.data.LongSumData;
import io.opentelemetry.sdk.metrics.data.MetricData;
import io.opentelemetry.sdk.metrics.data.ValueAtPercentile;
import io.opentelemetry.sdk.resources.Resource;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;

/** Converter from SDK {@link MetricData} to OTLP {@link ResourceMetrics}. */
public final class MetricAdapter {

  /** Converts the provided {@link MetricData} to {@link ResourceMetrics}. */
  public static List<ResourceMetrics> toProtoResourceMetrics(Collection<MetricData> metricData) {
    Map<Resource, Map<InstrumentationLibraryInfo, List<Metric>>> resourceAndLibraryMap =
        groupByResourceAndLibrary(metricData);
    List<ResourceMetrics> resourceMetrics = new ArrayList<>(resourceAndLibraryMap.size());
    for (Map.Entry<Resource, Map<InstrumentationLibraryInfo, List<Metric>>> entryResource :
        resourceAndLibraryMap.entrySet()) {
      List<InstrumentationLibraryMetrics> instrumentationLibraryMetrics =
          new ArrayList<>(entryResource.getValue().size());
      for (Map.Entry<InstrumentationLibraryInfo, List<Metric>> entryLibrary :
          entryResource.getValue().entrySet()) {
        instrumentationLibraryMetrics.add(buildInstrumentationLibraryMetrics(entryLibrary));
      }
      resourceMetrics.add(
          buildResourceMetrics(entryResource.getKey(), instrumentationLibraryMetrics));
    }
    return resourceMetrics;
  }

  private static ResourceMetrics buildResourceMetrics(
      Resource resource, List<InstrumentationLibraryMetrics> instrumentationLibraryMetrics) {
    ResourceMetrics.Builder resourceMetricsBuilder =
        ResourceMetrics.newBuilder()
            .setResource(ResourceAdapter.toProtoResource(resource))
            .addAllInstrumentationLibraryMetrics(instrumentationLibraryMetrics);
    String schemaUrl = resource.getSchemaUrl();
    if (schemaUrl != null) {
      resourceMetricsBuilder.setSchemaUrl(schemaUrl);
    }
    return resourceMetricsBuilder.build();
  }

  private static InstrumentationLibraryMetrics buildInstrumentationLibraryMetrics(
      Map.Entry<InstrumentationLibraryInfo, List<Metric>> entryLibrary) {
    InstrumentationLibraryMetrics.Builder metricsBuilder =
        InstrumentationLibraryMetrics.newBuilder()
            .setInstrumentationLibrary(
                CommonAdapter.toProtoInstrumentationLibrary(entryLibrary.getKey()))
            .addAllMetrics(entryLibrary.getValue());
    if (entryLibrary.getKey().getSchemaUrl() != null) {
      metricsBuilder.setSchemaUrl(entryLibrary.getKey().getSchemaUrl());
    }
    return metricsBuilder.build();
  }

  private static Map<Resource, Map<InstrumentationLibraryInfo, List<Metric>>>
      groupByResourceAndLibrary(Collection<MetricData> metricDataList) {
    Map<Resource, Map<InstrumentationLibraryInfo, List<Metric>>> result = new HashMap<>();
    for (MetricData metricData : metricDataList) {
      if (metricData.isEmpty()) {
        // If no points available then ignore.
        continue;
      }

      Resource resource = metricData.getResource();
      Map<InstrumentationLibraryInfo, List<Metric>> libraryInfoListMap =
          result.get(metricData.getResource());
      if (libraryInfoListMap == null) {
        libraryInfoListMap = new HashMap<>();
        result.put(resource, libraryInfoListMap);
      }
      List<Metric> metricList =
          libraryInfoListMap.computeIfAbsent(
              metricData.getInstrumentationLibraryInfo(), k -> new ArrayList<>());
      metricList.add(toProtoMetric(metricData));
    }
    return result;
  }

  // fall through comment isn't working for some reason.
  @SuppressWarnings("fallthrough")
  static Metric toProtoMetric(MetricData metricData) {
    Metric.Builder builder =
        Metric.newBuilder()
            .setName(metricData.getName())
            .setDescription(metricData.getDescription())
            .setUnit(metricData.getUnit());

    switch (metricData.getType()) {
      case LONG_SUM:
        LongSumData longSumData = metricData.getLongSumData();
        builder.setSum(
            Sum.newBuilder()
                .setIsMonotonic(longSumData.isMonotonic())
                .setAggregationTemporality(
                    mapToTemporality(longSumData.getAggregationTemporality()))
                .addAllDataPoints(toIntDataPoints(longSumData.getPoints()))
                .build());
        break;
      case DOUBLE_SUM:
        DoubleSumData doubleSumData = metricData.getDoubleSumData();
        builder.setSum(
            Sum.newBuilder()
                .setIsMonotonic(doubleSumData.isMonotonic())
                .setAggregationTemporality(
                    mapToTemporality(doubleSumData.getAggregationTemporality()))
                .addAllDataPoints(toDoubleDataPoints(doubleSumData.getPoints()))
                .build());
        break;
      case SUMMARY:
        DoubleSummaryData doubleSummaryData = metricData.getDoubleSummaryData();
        builder.setSummary(
            Summary.newBuilder()
                .addAllDataPoints(toSummaryDataPoints(doubleSummaryData.getPoints()))
                .build());
        break;
      case LONG_GAUGE:
        LongGaugeData longGaugeData = metricData.getLongGaugeData();
        builder.setGauge(
            Gauge.newBuilder()
                .addAllDataPoints(toIntDataPoints(longGaugeData.getPoints()))
                .build());
        break;
      case DOUBLE_GAUGE:
        DoubleGaugeData doubleGaugeData = metricData.getDoubleGaugeData();
        builder.setGauge(
            Gauge.newBuilder()
                .addAllDataPoints(toDoubleDataPoints(doubleGaugeData.getPoints()))
                .build());
        break;
      case HISTOGRAM:
        DoubleHistogramData doubleHistogramData = metricData.getDoubleHistogramData();
        builder.setHistogram(
            Histogram.newBuilder()
                .setAggregationTemporality(
                    mapToTemporality(doubleHistogramData.getAggregationTemporality()))
                .addAllDataPoints(toHistogramDataPoints(doubleHistogramData.getPoints()))
                .build());
        break;
    }
    return builder.build();
  }

  private static AggregationTemporality mapToTemporality(
      io.opentelemetry.sdk.metrics.data.AggregationTemporality temporality) {
    switch (temporality) {
      case CUMULATIVE:
        return AGGREGATION_TEMPORALITY_CUMULATIVE;
      case DELTA:
        return AGGREGATION_TEMPORALITY_DELTA;
    }
    return AGGREGATION_TEMPORALITY_UNSPECIFIED;
  }

  // Fill labels too until Collector supports attributes and users have had a chance to update.
  @SuppressWarnings("deprecation")
  static List<NumberDataPoint> toIntDataPoints(Collection<LongPointData> points) {
    List<NumberDataPoint> result = new ArrayList<>(points.size());
    for (LongPointData longPoint : points) {
      NumberDataPoint.Builder builder =
          NumberDataPoint.newBuilder()
              .setStartTimeUnixNano(longPoint.getStartEpochNanos())
              .setTimeUnixNano(longPoint.getEpochNanos())
              .setAsInt(longPoint.getValue());
      fillAttributes(longPoint.getAttributes(), builder::addAttributes, builder::addLabels);
      result.add(builder.build());
    }
    return result;
  }

  // Fill labels too until Collector supports attributes and users have had a chance to update.
  @SuppressWarnings("deprecation")
  static Collection<NumberDataPoint> toDoubleDataPoints(Collection<DoublePointData> points) {
    List<NumberDataPoint> result = new ArrayList<>(points.size());
    for (DoublePointData doublePoint : points) {
      NumberDataPoint.Builder builder =
          NumberDataPoint.newBuilder()
              .setStartTimeUnixNano(doublePoint.getStartEpochNanos())
              .setTimeUnixNano(doublePoint.getEpochNanos())
              .setAsDouble(doublePoint.getValue());
      fillAttributes(doublePoint.getAttributes(), builder::addAttributes, builder::addLabels);
      result.add(builder.build());
    }
    return result;
  }

  // Fill labels too until Collector supports attributes and users have had a chance to update.
  @SuppressWarnings("deprecation")
  static List<SummaryDataPoint> toSummaryDataPoints(Collection<DoubleSummaryPointData> points) {
    List<SummaryDataPoint> result = new ArrayList<>(points.size());
    for (DoubleSummaryPointData doubleSummaryPoint : points) {
      SummaryDataPoint.Builder builder =
          SummaryDataPoint.newBuilder()
              .setStartTimeUnixNano(doubleSummaryPoint.getStartEpochNanos())
              .setTimeUnixNano(doubleSummaryPoint.getEpochNanos())
              .setCount(doubleSummaryPoint.getCount())
              .setSum(doubleSummaryPoint.getSum());
      fillAttributes(
          doubleSummaryPoint.getAttributes(), builder::addAttributes, builder::addLabels);
      // Not calling directly addAllQuantileValues because that generates couple of unnecessary
      // allocations if empty list.
      if (!doubleSummaryPoint.getPercentileValues().isEmpty()) {
        for (ValueAtPercentile valueAtPercentile : doubleSummaryPoint.getPercentileValues()) {
          builder.addQuantileValues(
              SummaryDataPoint.ValueAtQuantile.newBuilder()
                  .setQuantile(valueAtPercentile.getPercentile() / 100.0)
                  .setValue(valueAtPercentile.getValue())
                  .build());
        }
      }
      result.add(builder.build());
    }
    return result;
  }

  // Fill labels too until Collector supports attributes and users have had a chance to update.
  @SuppressWarnings("deprecation")
  static Collection<HistogramDataPoint> toHistogramDataPoints(
      Collection<DoubleHistogramPointData> points) {
    List<HistogramDataPoint> result = new ArrayList<>(points.size());
    for (DoubleHistogramPointData doubleHistogramPoint : points) {
      HistogramDataPoint.Builder builder =
          HistogramDataPoint.newBuilder()
              .setStartTimeUnixNano(doubleHistogramPoint.getStartEpochNanos())
              .setTimeUnixNano(doubleHistogramPoint.getEpochNanos())
              .setCount(doubleHistogramPoint.getCount())
              .setSum(doubleHistogramPoint.getSum())
              .addAllBucketCounts(doubleHistogramPoint.getCounts());
      List<Double> boundaries = doubleHistogramPoint.getBoundaries();
      if (!boundaries.isEmpty()) {
        builder.addAllExplicitBounds(boundaries);
      }
      fillAttributes(
          doubleHistogramPoint.getAttributes(), builder::addAttributes, builder::addLabels);
      result.add(builder.build());
    }
    return result;
  }

  // Fill labels too until Collector supports attributes and users have had a chance to update.
  @SuppressWarnings("deprecation")
  private static void fillAttributes(
      Attributes attributes,
      Consumer<KeyValue> attributeSetter,
      Consumer<io.opentelemetry.proto.common.v1.StringKeyValue> labelSetter) {
    attributes.forEach(
        (key, value) -> {
          attributeSetter.accept(CommonAdapter.toProtoAttribute(key, value));
          labelSetter.accept(
              io.opentelemetry.proto.common.v1.StringKeyValue.newBuilder()
                  .setKey(key.getKey())
                  .setValue(value.toString())
                  .build());
        });
  }

  private MetricAdapter() {}
}
