/*
 * 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.skywalking.oap.meter.analyzer.dsl;

import static java.util.function.UnaryOperator.identity;
import static java.util.stream.Collectors.groupingBy;
import static java.util.stream.Collectors.mapping;
import static java.util.stream.Collectors.toList;
import static com.google.common.collect.ImmutableMap.toImmutableMap;

import org.apache.commons.lang3.StringUtils;
import org.apache.skywalking.oap.meter.analyzer.dsl.EntityDescription.EndpointEntityDescription;
import org.apache.skywalking.oap.meter.analyzer.dsl.EntityDescription.EntityDescription;
import org.apache.skywalking.oap.meter.analyzer.dsl.EntityDescription.InstanceEntityDescription;
import org.apache.skywalking.oap.meter.analyzer.dsl.EntityDescription.ProcessRelationEntityDescription;
import org.apache.skywalking.oap.meter.analyzer.dsl.EntityDescription.ServiceEntityDescription;
import org.apache.skywalking.oap.meter.analyzer.dsl.EntityDescription.ServiceRelationEntityDescription;
import org.apache.skywalking.oap.meter.analyzer.dsl.tagOpt.K8sRetagType;
import org.apache.skywalking.oap.server.core.Const;
import org.apache.skywalking.oap.server.core.UnexpectedException;
import org.apache.skywalking.oap.server.core.analysis.Layer;
import org.apache.skywalking.oap.server.core.analysis.meter.MeterEntity;
import org.apache.skywalking.oap.server.core.analysis.meter.ScopeType;
import org.apache.skywalking.oap.server.core.source.DetectPoint;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import java.util.function.DoubleBinaryOperator;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;
import groovy.lang.Closure;
import io.vavr.Function2;
import io.vavr.Function3;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import lombok.ToString;
import lombok.extern.slf4j.Slf4j;
import org.apache.skywalking.oap.server.library.util.StringUtil;

/**
 * SampleFamily represents a collection of {@link Sample}.
 */
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
@EqualsAndHashCode
@ToString
@Slf4j
public class SampleFamily {
    public static final SampleFamily EMPTY = new SampleFamily(new Sample[0], RunningContext.EMPTY);

    static SampleFamily build(RunningContext ctx, Sample... samples) {
        Preconditions.checkNotNull(samples);
        Preconditions.checkArgument(samples.length > 0);
        samples = Arrays.stream(samples).filter(sample -> !Double.isNaN(sample.getValue())).toArray(Sample[]::new);
        if (samples.length == 0) {
            return EMPTY;
        }
        return new SampleFamily(samples, Optional.ofNullable(ctx).orElseGet(RunningContext::instance));
    }

    public final Sample[] samples;

    public final RunningContext context;

    /**
     * Following operations are used in DSL
     */

    /* tag filter operations*/
    public SampleFamily tagEqual(String... labels) {
        return match(labels, InternalOps::stringComp);
    }

    public SampleFamily tagNotEqual(String[] labels) {
        return match(labels, (sv, lv) -> !InternalOps.stringComp(sv, lv));
    }

    public SampleFamily tagMatch(String[] labels) {
        return match(labels, String::matches);
    }

    public SampleFamily tagNotMatch(String[] labels) {
        return match(labels, (sv, lv) -> !sv.matches(lv));
    }

    /* value filter operations*/
    public SampleFamily valueEqual(double compValue) {
        return valueMatch(CompType.EQUAL, compValue, InternalOps::doubleComp);
    }

    public SampleFamily valueNotEqual(double compValue) {
        return valueMatch(CompType.NOT_EQUAL, compValue, InternalOps::doubleComp);
    }

    public SampleFamily valueGreater(double compValue) {
        return valueMatch(CompType.GREATER, compValue, InternalOps::doubleComp);
    }

    public SampleFamily valueGreaterEqual(double compValue) {
        return valueMatch(CompType.GREATER_EQUAL, compValue, InternalOps::doubleComp);
    }

    public SampleFamily valueLess(double compValue) {
        return valueMatch(CompType.LESS, compValue, InternalOps::doubleComp);
    }

    public SampleFamily valueLessEqual(double compValue) {
        return valueMatch(CompType.LESS_EQUAL, compValue, InternalOps::doubleComp);
    }

    /* Binary operator overloading*/
    public SampleFamily plus(Number number) {
        return newValue(v -> v + number.doubleValue());
    }

    public SampleFamily minus(Number number) {
        return newValue(v -> v - number.doubleValue());
    }

    public SampleFamily multiply(Number number) {
        return newValue(v -> v * number.doubleValue());
    }

    public SampleFamily div(Number number) {
        return newValue(v -> v / number.doubleValue());
    }

    public SampleFamily negative() {
        return newValue(v -> -v);
    }

    public SampleFamily plus(SampleFamily another) {
        if (this == EMPTY && another == EMPTY) {
            return SampleFamily.EMPTY;
        }
        if (this == EMPTY) {
            return another;
        }
        if (another == EMPTY) {
            return this;
        }
        return newValue(another, Double::sum);
    }

    public SampleFamily minus(SampleFamily another) {
        if (this == EMPTY && another == EMPTY) {
            return SampleFamily.EMPTY;
        }
        if (this == EMPTY) {
            return another.negative();
        }
        if (another == EMPTY) {
            return this;
        }
        return newValue(another, (a, b) -> a - b);
    }

    public SampleFamily multiply(SampleFamily another) {
        if (this == EMPTY || another == EMPTY) {
            return SampleFamily.EMPTY;
        }
        return newValue(another, (a, b) -> a * b);
    }

    public SampleFamily div(SampleFamily another) {
        if (this == EMPTY) {
            return SampleFamily.EMPTY;
        }
        if (another == EMPTY) {
            return div(0.0);
        }
        return newValue(another, (a, b) -> a / b);
    }

    /* Aggregation operators */
    public SampleFamily sum(List<String> by) {
        return aggregate(by, Double::sum);
    }

    public SampleFamily max(List<String> by) {
        return aggregate(by, Double::max);
    }

    public SampleFamily min(List<String> by) {
        return aggregate(by, Double::min);
    }

    public SampleFamily avg(List<String> by) {
        ExpressionParsingContext.get().ifPresent(ctx -> ctx.aggregationLabels.addAll(by));
        if (this == EMPTY) {
            return EMPTY;
        }
        if (by == null) {
            double result = Arrays.stream(samples).mapToDouble(Sample::getValue).average().orElse(0.0D);
            return SampleFamily.build(
                this.context, InternalOps.newSample(samples[0].name, ImmutableMap.of(), samples[0].timestamp, result));
        }

        return SampleFamily.build(
            this.context,
            Arrays.stream(samples)
                  .collect(groupingBy(it -> InternalOps.getLabels(by, it), mapping(identity(), toList())))
                  .entrySet().stream()
                  .map(entry -> InternalOps.newSample(
                      entry.getValue().get(0).getName(),
                      entry.getKey(),
                      entry.getValue().get(0).getTimestamp(),
                      entry.getValue().stream().mapToDouble(Sample::getValue).average().orElse(0.0D)
                  ))
                  .toArray(Sample[]::new)
        );
    }

    protected SampleFamily aggregate(List<String> by, DoubleBinaryOperator aggregator) {
        ExpressionParsingContext.get().ifPresent(ctx -> ctx.aggregationLabels.addAll(by));
        if (this == EMPTY) {
            return EMPTY;
        }
        if (by == null) {
            double result = Arrays.stream(samples).mapToDouble(s -> s.value).reduce(aggregator).orElse(0.0D);
            return SampleFamily.build(
                this.context, InternalOps.newSample(samples[0].name, ImmutableMap.of(), samples[0].timestamp, result));
        }
        return SampleFamily.build(
            this.context,
            Arrays.stream(samples)
                  .collect(groupingBy(it -> InternalOps.getLabels(by, it), mapping(identity(), toList())))
                  .entrySet().stream()
                  .map(entry -> InternalOps.newSample(
                      entry.getValue().get(0).getName(),
                      entry.getKey(),
                      entry.getValue().get(0).getTimestamp(),
                      entry.getValue().stream().mapToDouble(Sample::getValue).reduce(aggregator).orElse(0.0D)
                  ))
                  .toArray(Sample[]::new)
        );
    }

    /* Function */
    public SampleFamily increase(String range) {
        Preconditions.checkArgument(!Strings.isNullOrEmpty(range));
        if (this == EMPTY) {
            return EMPTY;
        }
        return SampleFamily.build(
            this.context,
            Arrays.stream(samples)
                  .map(sample -> sample.increase(range, (lowerBoundValue, unused) -> sample.value - lowerBoundValue))
                  .toArray(Sample[]::new)
        );
    }

    public SampleFamily rate(String range) {
        Preconditions.checkArgument(!Strings.isNullOrEmpty(range));
        if (this == EMPTY) {
            return EMPTY;
        }
        return SampleFamily.build(
            this.context,
            Arrays.stream(samples)
                  .map(sample -> sample.increase(
                      range,
                      (lowerBoundValue, lowerBoundTime) -> {
                          final long timeDiff = (sample.timestamp - lowerBoundTime) / 1000;
                          return timeDiff < 1L ? 0.0 : (sample.value - lowerBoundValue) / timeDiff;
                      }
                  ))
                  .toArray(Sample[]::new)
        );
    }

    public SampleFamily irate() {
        if (this == EMPTY) {
            return EMPTY;
        }
        return SampleFamily.build(
            this.context,
            Arrays.stream(samples)
                  .map(sample -> sample.increase(
                      (lowerBoundValue, lowerBoundTime) -> {
                          final long timeDiff = (sample.timestamp - lowerBoundTime) / 1000;
                          return timeDiff < 1L ? 0.0 : (sample.value - lowerBoundValue) / timeDiff;
                      }
                  ))
                  .toArray(Sample[]::new)
        );
    }

    @SuppressWarnings(value = "unchecked")
    public SampleFamily tag(Closure<?> cl) {
        if (this == EMPTY) {
            return EMPTY;
        }
        return SampleFamily.build(
            this.context,
            Arrays.stream(samples)
                  .map(sample -> {
                      Object delegate = new Object();
                      Closure<?> c = cl.rehydrate(delegate, sample, delegate);
                      Map<String, String> arg = Maps.newHashMap(sample.labels);
                      Object r = c.call(arg);
                      return sample.toBuilder()
                                   .labels(
                                       ImmutableMap.copyOf(
                                           Optional.ofNullable((r instanceof Map) ? (Map<String, String>) r : null)
                                                   .orElse(arg)))
                                   .build();
                  }).toArray(Sample[]::new)
        );
    }

    public SampleFamily filter(Closure<Boolean> filter) {
        if (this == EMPTY) {
            return EMPTY;
        }
        final Sample[] filtered = Arrays.stream(samples)
                                        .filter(it -> filter.call(it.labels))
                                        .toArray(Sample[]::new);
        if (filtered.length == 0) {
            return EMPTY;
        }
        return SampleFamily.build(context, filtered);
    }

    /* k8s retags*/
    public SampleFamily retagByK8sMeta(String newLabelName,
                                       K8sRetagType type,
                                       String existingLabelName,
                                       String namespaceLabelName) {
        Preconditions.checkArgument(!Strings.isNullOrEmpty(newLabelName));
        Preconditions.checkArgument(!Strings.isNullOrEmpty(existingLabelName));
        Preconditions.checkArgument(!Strings.isNullOrEmpty(namespaceLabelName));
        if (this == EMPTY) {
            return EMPTY;
        }

        return SampleFamily.build(
            this.context, type.execute(samples, newLabelName, existingLabelName, namespaceLabelName));
    }

    public SampleFamily histogram() {
        return histogram("le", this.context.defaultHistogramBucketUnit);
    }

    public SampleFamily histogram(String le) {
        return histogram(le, this.context.defaultHistogramBucketUnit);
    }

    public SampleFamily histogram(String le, TimeUnit unit) {
        long scale = unit.toMillis(1);
        Preconditions.checkArgument(scale > 0);
        ExpressionParsingContext.get().ifPresent(ctx -> ctx.isHistogram = true);
        if (this == EMPTY) {
            return EMPTY;
        }
        return SampleFamily.build(
            this.context,
            Stream.concat(
                Arrays.stream(samples).filter(s -> !s.labels.containsKey(le)),
                Arrays.stream(samples)
                      .filter(s -> s.labels.containsKey(le))
                      .sorted(Comparator.comparingDouble(s -> Double.parseDouble(s.labels.get(le))))
                      .map(s -> {
                          double r = s.value;
                          ImmutableMap<String, String> ll = ImmutableMap.<String, String>builder()
                                                                        .putAll(Maps.filterKeys(s.labels,
                                                                                                key -> !Objects.equals(
                                                                                                    key, le)
                                                                        ))
                                                                        .put(
                                                                            "le",
                                                                            String.valueOf((long) ((Double.parseDouble(s.labels.get(le))) * scale)))
                                                                        .build();
                          return InternalOps.newSample(s.name, ll, s.timestamp, r);
                      })
            ).toArray(Sample[]::new)
        );
    }

    public SampleFamily histogram_percentile(List<Integer> percentiles) {
        Preconditions.checkArgument(percentiles.size() > 0);
        int[] p = percentiles.stream().mapToInt(i -> i).toArray();
        ExpressionParsingContext.get().ifPresent(ctx -> {
            Preconditions.checkState(
                ctx.isHistogram, "histogram() should be invoked before invoking histogram_percentile()");
            ctx.percentiles = p;
        });
        return this;
    }

    public SampleFamily service(List<String> labelKeys, Layer layer) {
        Preconditions.checkArgument(labelKeys.size() > 0);
        ExpressionParsingContext.get().ifPresent(ctx -> {
            ctx.scopeType = ScopeType.SERVICE;
            ctx.scopeLabels.addAll(labelKeys);
        });
        if (this == EMPTY) {
            return EMPTY;
        }
        return createMeterSamples(new ServiceEntityDescription(labelKeys, layer, Const.POINT));
    }

    public SampleFamily service(List<String> labelKeys, String delimiter, Layer layer) {
        Preconditions.checkArgument(labelKeys.size() > 0);
        ExpressionParsingContext.get().ifPresent(ctx -> {
            ctx.scopeType = ScopeType.SERVICE;
            ctx.scopeLabels.addAll(labelKeys);
        });
        if (this == EMPTY) {
            return EMPTY;
        }
        return createMeterSamples(new ServiceEntityDescription(labelKeys, layer, delimiter));
    }

    public SampleFamily instance(List<String> serviceKeys, String serviceDelimiter,
                                 List<String> instanceKeys, String instanceDelimiter,
                                 Layer layer, Closure<Map<String, String>> propertiesExtractor) {
        Preconditions.checkArgument(serviceKeys.size() > 0);
        Preconditions.checkArgument(instanceKeys.size() > 0);
        ExpressionParsingContext.get().ifPresent(ctx -> {
            ctx.scopeType = ScopeType.SERVICE_INSTANCE;
            ctx.scopeLabels.addAll(serviceKeys);
            ctx.scopeLabels.addAll(instanceKeys);
        });
        if (this == EMPTY) {
            return EMPTY;
        }
        return createMeterSamples(new InstanceEntityDescription(
            serviceKeys, instanceKeys, layer, serviceDelimiter, instanceDelimiter, propertiesExtractor));
    }

    public SampleFamily instance(List<String> serviceKeys, List<String> instanceKeys, Layer layer) {
        return instance(serviceKeys, Const.POINT, instanceKeys, Const.POINT, layer, null);
    }

    public SampleFamily endpoint(List<String> serviceKeys, List<String> endpointKeys, Layer layer) {
        Preconditions.checkArgument(serviceKeys.size() > 0);
        Preconditions.checkArgument(endpointKeys.size() > 0);
        ExpressionParsingContext.get().ifPresent(ctx -> {
            ctx.scopeType = ScopeType.ENDPOINT;
            ctx.scopeLabels.addAll(serviceKeys);
            ctx.scopeLabels.addAll(endpointKeys);
        });
        if (this == EMPTY) {
            return EMPTY;
        }
        return createMeterSamples(new EndpointEntityDescription(serviceKeys, endpointKeys, layer, Const.POINT));
    }

    public SampleFamily serviceRelation(DetectPoint detectPoint, List<String> sourceServiceKeys, List<String> destServiceKeys, Layer layer) {
        Preconditions.checkArgument(sourceServiceKeys.size() > 0);
        Preconditions.checkArgument(destServiceKeys.size() > 0);
        ExpressionParsingContext.get().ifPresent(ctx -> {
            ctx.scopeType = ScopeType.SERVICE_RELATION;
            ctx.scopeLabels.addAll(sourceServiceKeys);
            ctx.scopeLabels.addAll(destServiceKeys);
        });
        if (this == EMPTY) {
            return EMPTY;
        }
        return createMeterSamples(new ServiceRelationEntityDescription(sourceServiceKeys, destServiceKeys, detectPoint, layer, Const.POINT));
    }

    public SampleFamily forEach(List<String> array, Closure<Void> each) {
        if (this == EMPTY) {
            return EMPTY;
        }
        return SampleFamily.build(this.context, Arrays.stream(this.samples).map(sample -> {
            Map<String, String> labels = Maps.newHashMap(sample.getLabels());
            for (String element : array) {
                each.call(element, labels);
            }
            return sample.toBuilder().labels(ImmutableMap.copyOf(labels)).build();
        }).toArray(Sample[]::new));
    }

    public SampleFamily processRelation(String detectPointKey, List<String> serviceKeys, List<String> instanceKeys, String sourceProcessIdKey, String destProcessIdKey, String componentKey) {
        Preconditions.checkArgument(serviceKeys.size() > 0);
        Preconditions.checkArgument(instanceKeys.size() > 0);
        Preconditions.checkArgument(StringUtil.isNotEmpty(sourceProcessIdKey));
        Preconditions.checkArgument(StringUtil.isNotEmpty(destProcessIdKey));
        ExpressionParsingContext.get().ifPresent(ctx -> {
            ctx.scopeType = ScopeType.PROCESS_RELATION;
            ctx.scopeLabels.addAll(serviceKeys);
            ctx.scopeLabels.addAll(instanceKeys);
            ctx.scopeLabels.add(detectPointKey);
            ctx.scopeLabels.add(sourceProcessIdKey);
            ctx.scopeLabels.add(destProcessIdKey);
            ctx.scopeLabels.add(componentKey);
        });
        if (this == EMPTY) {
            return EMPTY;
        }
        return createMeterSamples(new ProcessRelationEntityDescription(serviceKeys, instanceKeys, sourceProcessIdKey, destProcessIdKey, detectPointKey, componentKey, Const.POINT));
    }

    private SampleFamily createMeterSamples(EntityDescription entityDescription) {
        Map<MeterEntity, Sample[]> meterSamples = new HashMap<>();
        Arrays.stream(samples)
              .collect(groupingBy(it -> InternalOps.getLabels(entityDescription.getLabelKeys(), it),
                                  mapping(identity(), toList())
              ))
              .forEach((labels, samples) -> {
                  MeterEntity meterEntity = InternalOps.buildMeterEntity(samples, entityDescription);
                  meterSamples.put(
                      meterEntity, InternalOps.left(samples, entityDescription.getLabelKeys()));
              });

        this.context.setMeterSamples(meterSamples);
        //This samples is original, The grouped samples is in context which mapping with MeterEntity
        return SampleFamily.build(this.context, samples);
    }

    private SampleFamily match(String[] labels, Function2<String, String, Boolean> op) {
        Preconditions.checkArgument(labels.length % 2 == 0);
        Map<String, String> ll = new HashMap<>(labels.length / 2);
        for (int i = 0; i < labels.length; i += 2) {
            ll.put(labels[i], labels[i + 1]);
        }
        Sample[] ss = Arrays.stream(samples)
                            .filter(sample -> ll.entrySet()
                                                .stream()
                                                .allMatch(
                                                    entry -> op.apply(sample.labels.getOrDefault(entry.getKey(), ""),
                                                                      entry.getValue()
                                                    )))
                            .toArray(Sample[]::new);
        return ss.length > 0 ? SampleFamily.build(this.context, ss) : EMPTY;
    }

    private SampleFamily valueMatch(CompType compType,
                                    double compValue,
                                    Function3<CompType, Double, Double, Boolean> op) {
        Sample[] ss = Arrays.stream(samples)
                            .filter(sample -> op.apply(compType, sample.value, compValue)).toArray(Sample[]::new);
        return ss.length > 0 ? SampleFamily.build(this.context, ss) : EMPTY;
    }

    SampleFamily newValue(Function<Double, Double> transform) {
        if (this == EMPTY) {
            return EMPTY;
        }
        Sample[] ss = new Sample[samples.length];
        for (int i = 0; i < ss.length; i++) {
            ss[i] = samples[i].newValue(transform);
        }
        return SampleFamily.build(this.context, ss);
    }

    private SampleFamily newValue(SampleFamily another, Function2<Double, Double, Double> transform) {
        Sample[] ss = Arrays.stream(samples)
                            .flatMap(cs -> io.vavr.collection.Stream.of(another.samples)
                                                                    .find(as -> cs.labels.equals(as.labels))
                                                                    .map(as -> cs.toBuilder()
                                                                                 .value(transform.apply(cs.value,
                                                                                                        as.value
                                                                                 )))
                                                                    .map(Sample.SampleBuilder::build)
                                                                    .toJavaStream())
                            .toArray(Sample[]::new);
        return ss.length > 0 ? SampleFamily.build(this.context, ss) : EMPTY;
    }

    public SampleFamily downsampling(final DownsamplingType type) {
        ExpressionParsingContext.get().ifPresent(it -> it.downsampling = type);
        return this;
    }

    /**
     * The parsing context holds key results more than sample collection.
     */
    @ToString
    @EqualsAndHashCode
    @Getter
    @Setter
    @Builder
    public static class RunningContext {

        static RunningContext EMPTY = instance();

        static RunningContext instance() {
            return RunningContext.builder()
                                 .defaultHistogramBucketUnit(TimeUnit.SECONDS)
                                 .build();
        }

        private Map<MeterEntity, Sample[]> meterSamples = new HashMap<>();

        private TimeUnit defaultHistogramBucketUnit;
    }

    private static class InternalOps {

        private static Sample[] left(List<Sample> samples, List<String> labelKeys) {
            return samples.stream().map(s -> {
                ImmutableMap<String, String> ll = ImmutableMap.<String, String>builder()
                                                              .putAll(Maps.filterKeys(s.labels,
                                                                                      key -> !labelKeys.contains(key)
                                                              ))
                                                              .build();
                return s.toBuilder().labels(ll).build();
            }).toArray(Sample[]::new);
        }

        private static String dim(List<Sample> samples, List<String> labelKeys, String delimiter) {
            String name = labelKeys.stream()
                                   .map(k -> samples.get(0).labels.getOrDefault(k, ""))
                                   .filter(v -> !StringUtil.isEmpty(v))
                                   .collect(Collectors.joining(StringUtil.isEmpty(delimiter) ? Const.POINT : delimiter));
            return name;
        }

        private static MeterEntity buildMeterEntity(List<Sample> samples,
                                                    EntityDescription entityDescription) {
            switch (entityDescription.getScopeType()) {
                case SERVICE:
                    ServiceEntityDescription serviceEntityDescription = (ServiceEntityDescription) entityDescription;
                    return MeterEntity.newService(
                        InternalOps.dim(samples, serviceEntityDescription.getServiceKeys(), serviceEntityDescription.getDelimiter()),
                        serviceEntityDescription.getLayer()
                    );
                case SERVICE_INSTANCE:
                    InstanceEntityDescription instanceEntityDescription = (InstanceEntityDescription) entityDescription;
                    Map<String, String> properties = null;
                    if (instanceEntityDescription.getPropertiesExtractor() != null) {
                        properties = instanceEntityDescription.getPropertiesExtractor().call(samples.get(0).labels);
                    }
                    return MeterEntity.newServiceInstance(
                        InternalOps.dim(samples, instanceEntityDescription.getServiceKeys(), instanceEntityDescription.getServiceDelimiter()),
                        InternalOps.dim(samples, instanceEntityDescription.getInstanceKeys(), instanceEntityDescription.getInstanceDelimiter()),
                        instanceEntityDescription.getLayer(),
                        properties
                    );
                case ENDPOINT:
                    EndpointEntityDescription endpointEntityDescription = (EndpointEntityDescription) entityDescription;
                    return MeterEntity.newEndpoint(
                        InternalOps.dim(samples, endpointEntityDescription.getServiceKeys(), endpointEntityDescription.getDelimiter()),
                        InternalOps.dim(samples, endpointEntityDescription.getEndpointKeys(), endpointEntityDescription.getDelimiter()),
                        endpointEntityDescription.getLayer()
                    );
                case SERVICE_RELATION:
                    ServiceRelationEntityDescription serviceRelationEntityDescription = (ServiceRelationEntityDescription) entityDescription;
                    return MeterEntity.newServiceRelation(
                        InternalOps.dim(samples, serviceRelationEntityDescription.getSourceServiceKeys(), serviceRelationEntityDescription.getDelimiter()),
                        InternalOps.dim(samples, serviceRelationEntityDescription.getDestServiceKeys(), serviceRelationEntityDescription.getDelimiter()),
                        serviceRelationEntityDescription.getDetectPoint(), serviceRelationEntityDescription.getLayer()
                    );
                case PROCESS_RELATION:
                    final ProcessRelationEntityDescription processRelationEntityDescription = (ProcessRelationEntityDescription) entityDescription;
                    final String detectPointValue = InternalOps.dim(samples, Collections.singletonList(processRelationEntityDescription.getDetectPointKey()), processRelationEntityDescription.getDelimiter());
                    DetectPoint point = StringUtils.equalsAnyIgnoreCase(detectPointValue, "server") ? DetectPoint.SERVER : DetectPoint.CLIENT;
                    final String componentValue = InternalOps.dim(samples, Collections.singletonList(processRelationEntityDescription.getComponentKey()), processRelationEntityDescription.getDelimiter());
                    final int componentId = StringUtil.isNotEmpty(componentValue) ? Integer.parseInt(componentValue) : 0;
                    return MeterEntity.newProcessRelation(
                        InternalOps.dim(samples, processRelationEntityDescription.getServiceKeys(), processRelationEntityDescription.getDelimiter()),
                        InternalOps.dim(samples, processRelationEntityDescription.getInstanceKeys(), processRelationEntityDescription.getDelimiter()),
                        InternalOps.dim(samples, Collections.singletonList(processRelationEntityDescription.getSourceProcessIdKey()), processRelationEntityDescription.getDelimiter()),
                        InternalOps.dim(samples, Collections.singletonList(processRelationEntityDescription.getDestProcessIdKey()), processRelationEntityDescription.getDelimiter()),
                        componentId,
                        point
                    );
                default:
                    throw new UnexpectedException(
                        "Unexpected scope type of entityDescription " + entityDescription);
            }
        }

        private static Sample newSample(String name,
                                        ImmutableMap<String, String> labels,
                                        long timestamp,
                                        double newValue) {
            return Sample.builder()
                         .value(newValue)
                         .labels(labels)
                         .timestamp(timestamp)
                         .name(name)
                         .build();
        }

        private static boolean stringComp(String a, String b) {
            if (Strings.isNullOrEmpty(a) && Strings.isNullOrEmpty(b)) {
                return true;
            }
            if (Strings.isNullOrEmpty(a)) {
                return false;
            }
            return a.equals(b);
        }

        private static boolean doubleComp(CompType compType, double a, double b) {
            int result = Double.compare(a, b);
            switch (compType) {
                case EQUAL:
                    return result == 0;
                case NOT_EQUAL:
                    return result != 0;
                case GREATER:
                    return result == 1;
                case GREATER_EQUAL:
                    return result == 0 || result == 1;
                case LESS:
                    return result == -1;
                case LESS_EQUAL:
                    return result == 0 || result == -1;
            }

            return false;
        }

        private static ImmutableMap<String, String> getLabels(final List<String> labelKeys, final Sample sample) {
            return labelKeys.stream()
                            .collect(toImmutableMap(
                                Function.identity(),
                                labelKey -> sample.labels.getOrDefault(labelKey, "")
                            ));
        }
    }

    private enum CompType {
        EQUAL, NOT_EQUAL, LESS, LESS_EQUAL, GREATER, GREATER_EQUAL
    }
}
