/*
 * 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.query.promql.rt;

import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import org.apache.skywalking.oap.query.promql.entity.MetricRangeData;
import org.apache.skywalking.oap.query.promql.entity.TimeValuePair;
import org.apache.skywalking.oap.query.promql.rt.exception.IllegalExpressionException;
import org.apache.skywalking.oap.query.promql.rt.result.ParseResultType;
import org.apache.skywalking.oap.query.promql.rt.result.MetricsRangeResult;
import org.apache.skywalking.oap.query.promql.rt.result.ScalarResult;
import org.apache.skywalking.oap.server.core.query.DurationUtils;
import org.apache.skywalking.oap.server.core.query.PointOfTime;
import org.apache.skywalking.oap.server.core.query.enumeration.Step;
import org.apache.skywalking.oap.server.core.query.input.Duration;
import org.apache.skywalking.oap.server.core.query.type.MetricsValues;
import org.apache.skywalking.promql.rt.grammar.PromQLParser;
import org.joda.time.DateTime;
import org.joda.time.format.PeriodFormatter;
import org.joda.time.format.PeriodFormatterBuilder;

public class PromOpUtils {
    //Adopt skywalking time step.
    public static Duration timestamp2Duration(long startTS, long endTS) {
        Duration duration = new Duration();
        if (endTS < startTS) {
            throw new IllegalArgumentException("End time must not be before start");
        }
        DateTime startDT = new DateTime(startTS);
        DateTime endDT = new DateTime(endTS);

        long durationValue = endTS - startTS;

        if (durationValue <= 3600000) {
            duration.setStep(Step.MINUTE);
            duration.setStart(startDT.toString(DurationUtils.YYYY_MM_DD_HHMM));
            duration.setEnd(endDT.toString(DurationUtils.YYYY_MM_DD_HHMM));
        } else if (durationValue <= 86400000) {
            duration.setStep(Step.HOUR);
            duration.setStart(startDT.toString(DurationUtils.YYYY_MM_DD_HH));
            duration.setEnd(endDT.toString(DurationUtils.YYYY_MM_DD_HH));
        } else {
            duration.setStep(Step.DAY);
            duration.setStart(startDT.toString(DurationUtils.YYYY_MM_DD));
            duration.setEnd(endDT.toString(DurationUtils.YYYY_MM_DD));
        }
        return duration;
    }

    static MetricsRangeResult matrixScalarBinaryOp(MetricsRangeResult matrix, ScalarResult scalar, int opType) {
        MetricsRangeResult result = new MetricsRangeResult();
        result.setResultType(ParseResultType.METRICS_RANGE);
        matrix.getMetricDataList().forEach(metricData -> {
            MetricRangeData newData = new MetricRangeData();
            result.getMetricDataList().add(newData);
            newData.setMetric(metricData.getMetric());
            List<TimeValuePair> newValues = metricData.getValues().stream().map(value -> {
                double v = Double.parseDouble(value.getValue());
                return new TimeValuePair(
                    value.getTime(),
                    formatDoubleValue(scalarBinaryOp(v, scalar.getValue(), opType))
                );

            }).collect(Collectors.toList());
            newData.setValues(newValues);
        });
        return result;
    }

    static MetricsRangeResult matrixBinaryOp(MetricsRangeResult matrixLeft,
                                             MetricsRangeResult matrixRight,
                                             int opType) throws IllegalExpressionException {
        MetricsRangeResult result = new MetricsRangeResult();
        result.setResultType(ParseResultType.METRICS_RANGE);
        for (int i = 0; i < matrixLeft.getMetricDataList().size(); i++) {
            MetricRangeData dataLeft = matrixLeft.getMetricDataList().get(i);
            MetricRangeData dataRight = matrixRight.getMetricDataList().get(i);
            if (!dataLeft.getMetric().equals(dataRight.getMetric())) {
                throw new IllegalExpressionException(
                    "The metric info result left in conformity with right.");
            }
            if (dataLeft.getValues().size() != dataRight.getValues().size()) {
                throw new IllegalExpressionException(
                    "The metric value range left in conformity with right.");
            }
            MetricRangeData newData = new MetricRangeData();
            result.getMetricDataList().add(newData);
            newData.setMetric(dataLeft.getMetric());
            List<TimeValuePair> newValues = new ArrayList<>();
            newData.setValues(newValues);
            for (int j = 0; j < dataLeft.getValues().size(); j++) {
                double lv = Double.parseDouble(dataLeft.getValues().get(j).getValue());
                double rv = Double.parseDouble(dataRight.getValues().get(j).getValue());
                newValues.add(new TimeValuePair(
                    dataLeft.getValues().get(j).getTime(),
                    formatDoubleValue(scalarBinaryOp(lv, rv, opType))
                ));
            }
        }
        return result;
    }

    static double scalarBinaryOp(double leftValue, double rightValue, int opType) {
        double calculatedResult = 0;
        switch (opType) {
            case PromQLParser.ADD:
                calculatedResult = leftValue + rightValue;
                break;
            case PromQLParser.SUB:
                calculatedResult = leftValue - rightValue;
                break;
            case PromQLParser.MUL:
                calculatedResult = leftValue * rightValue;
                break;
            case PromQLParser.DIV:
                calculatedResult = leftValue / rightValue;
                break;
            case PromQLParser.MOD:
                calculatedResult = leftValue % rightValue;
                break;
        }
        return calculatedResult;
    }

    static int scalarCompareOp(double leftValue, double rightValue, int opType) {
        int comparedResult = 0;
        switch (opType) {
            case PromQLParser.DEQ:
                comparedResult = boolToInt(leftValue == rightValue);
                break;
            case PromQLParser.NEQ:
                comparedResult = boolToInt(leftValue != rightValue);
                break;
            case PromQLParser.GT:
                comparedResult = boolToInt(leftValue > rightValue);
                break;
            case PromQLParser.LT:
                comparedResult = boolToInt(leftValue < rightValue);
                break;
            case PromQLParser.GTE:
                comparedResult = boolToInt(leftValue >= rightValue);
                break;
            case PromQLParser.LTE:
                comparedResult = boolToInt(leftValue <= rightValue);
                break;
        }
        return comparedResult;
    }

    private static int boolToInt(boolean v) {
        return v ? 1 : 0;
    }

    static MetricsRangeResult matrixScalarCompareOp(MetricsRangeResult matrix, ScalarResult scalar, int opType) {
        MetricsRangeResult result = new MetricsRangeResult();
        result.setResultType(ParseResultType.METRICS_RANGE);
        matrix.getMetricDataList().forEach(metricData -> {
            MetricRangeData newData = new MetricRangeData();
            result.getMetricDataList().add(newData);
            newData.setMetric(metricData.getMetric());
            List<TimeValuePair> newValues = metricData.getValues()
                                                      .stream()
                                                      .filter(timeValuePair -> scalarCompareOp(
                                                          Double.parseDouble(timeValuePair.getValue()),
                                                          scalar.getValue(), opType
                                                      ) == 1)
                                                      .collect(
                                                          Collectors.toList());
            newData.setValues(newValues);
        });
        return result;
    }

    static MetricsRangeResult matrixCompareOp(MetricsRangeResult matrixLeft,
                                              MetricsRangeResult matrixRight,
                                              int opType) throws IllegalExpressionException {
        MetricsRangeResult result = new MetricsRangeResult();
        result.setResultType(ParseResultType.METRICS_RANGE);
        for (int i = 0; i < matrixLeft.getMetricDataList().size(); i++) {
            MetricRangeData dataLeft = matrixLeft.getMetricDataList().get(i);
            MetricRangeData dataRight = matrixRight.getMetricDataList().get(i);
            if (!dataLeft.getMetric().equals(dataRight.getMetric())) {
                throw new IllegalExpressionException(
                    "The metric info result left in conformity with right.");
            }
            if (dataLeft.getValues().size() != dataRight.getValues().size()) {
                throw new IllegalExpressionException(
                    "The metric value range left in conformity with right.");
            }
            MetricRangeData newData = new MetricRangeData();
            result.getMetricDataList().add(newData);
            newData.setMetric(dataLeft.getMetric());
            List<TimeValuePair> newValues = new ArrayList<>();
            newData.setValues(newValues);
            for (int j = 0; j < dataLeft.getValues().size(); j++) {
                double lv = Double.parseDouble(dataLeft.getValues().get(j).getValue());
                double rv = Double.parseDouble(dataRight.getValues().get(j).getValue());
                if (scalarCompareOp(lv, rv, opType) == 1) {
                    newValues.add(dataLeft.getValues().get(j));
                }
            }
        }
        return result;
    }

    public static List<TimeValuePair> buildMatrixValues(Duration duration, String singleValue) {
        List<PointOfTime> times = duration.assembleDurationPoints();
        List<TimeValuePair> values = new ArrayList<>(times.size());
        for (PointOfTime time : times) {
            long retTimestampSec = DurationUtils.INSTANCE.parseToDateTime(
                                                    duration.getStep(), time.getPoint())
                                                         .getMillis() / 1000;
            TimeValuePair value = new TimeValuePair(
                retTimestampSec, singleValue);
            values.add(value);
        }
        return values;
    }

    static List<TimeValuePair> buildMatrixValues(Duration duration, MetricsValues metricsValues) {
        List<PointOfTime> times = duration.assembleDurationPoints();
        List<TimeValuePair> values = new ArrayList<>(times.size());
        for (int i = 0; i < times.size(); i++) {
            long retTimestampSec = DurationUtils.INSTANCE.parseToDateTime(
                                                    duration.getStep(), times.get(i).getPoint())
                                                         .getMillis() / 1000;
            TimeValuePair value = new TimeValuePair(
                retTimestampSec, Long.toString(metricsValues.getValues().getValues().get(i).getValue()));
            values.add(value);
        }
        return values;
    }

    public static String formatDoubleValue(double v) {
        DecimalFormat format = new DecimalFormat("#.##");
        return format.format(v);
    }

    /**
     * Format duration string to org.joda.time.Duration.
     * Don't support year and month because the days vary in length.
     * @param duration such as "5d", "30m", "5d30m, "1w, "1w5d"
     * @return org.joda.time.Duration
     */
    public static org.joda.time.Duration formatDuration(String duration) {
        PeriodFormatter f = new PeriodFormatterBuilder()
            .appendWeeks().appendSuffix("w")
            .appendDays().appendSuffix("d")
            .appendHours().appendSuffix("h")
            .appendMinutes().appendSuffix("m")
            .appendSeconds().appendSuffix("s")
            .appendMillis().appendSuffix("ms")
            .toFormatter();
        return f.parsePeriod(duration).toStandardDuration();
    }
}
