/**
 * 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
 * <p>
 * http://www.apache.org/licenses/LICENSE-2.0
 * <p>
 * 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 com.alibaba.jstorm.task;

import com.alibaba.jstorm.cluster.Common;
import com.alibaba.jstorm.common.metric.AsmHistogram;
import com.alibaba.jstorm.common.metric.AsmMetric;
import com.alibaba.jstorm.metric.JStormMetrics;
import com.alibaba.jstorm.metric.MetricDef;
import com.alibaba.jstorm.metric.MetricType;
import com.alibaba.jstorm.metric.MetricUtils;
import com.alibaba.jstorm.utils.TimeUtils;
import java.io.Serializable;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class TaskBaseMetric implements Serializable {
    private static final Logger logger = LoggerFactory.getLogger(JStormMetrics.class);

    private static final long serialVersionUID = -7157987126460293444L;
    private String topologyId;
    private String componentId;
    private int taskId;

    /**
     * local metric name cache to avoid frequent metric name concatenation taskId + streamId + name ==> full metric name
     */
    private final ConcurrentMap<String, AsmMetric> metricCache = new ConcurrentHashMap<>();

    public TaskBaseMetric(String topologyId, String componentId, int taskId) {
        this.topologyId = topologyId;
        this.componentId = componentId;
        this.taskId = taskId;
        logger.info("init task base metric, tp id:{}, comp id:{}, task id:{}", topologyId, componentId, taskId);
    }

    private AsmMetric findMetric(String streamId, String name, MetricType metricType, boolean mergeTopology) {
        //String key = new StringBuilder(30).append(taskId).append(streamId).append(name).toString();
        String key = streamId + name;
        AsmMetric existingMetric = metricCache.get(key);
        if (existingMetric == null) {
            String fullName = MetricUtils.streamMetricName(topologyId, componentId, taskId, streamId, name, metricType);
            existingMetric = JStormMetrics.getStreamMetric(fullName);
            if (existingMetric == null) {
                existingMetric = AsmMetric.Builder.build(metricType);
                JStormMetrics.registerStreamMetric(fullName, existingMetric, mergeTopology);
            }
            AsmMetric oldMetric = metricCache.putIfAbsent(key, existingMetric);
            if (oldMetric != null) {
                existingMetric = oldMetric;
            }
        }

        return existingMetric;
    }

    public void update(final String streamId, final String name, final Number value,
                       final MetricType metricType, boolean mergeTopology) {
        AsmMetric existingMetric = findMetric(streamId, name, metricType, mergeTopology);
        existingMetric.update(value);
    }

    public void updateTime(String streamId, String name, long start, long end, boolean mergeTopology) {
        updateTime(streamId, name, start, end, 1, mergeTopology);
    }

    /**
     * almost the same implementation of above update, but improves performance for histograms
     */
    public void updateTime(String streamId, String name, long start, long end, int count, boolean mergeTopology) {
        if (start > 0 && count > 0) {
            AsmMetric existingMetric = findMetric(streamId, name, MetricType.HISTOGRAM, mergeTopology);

            if (existingMetric instanceof AsmHistogram) {
                AsmHistogram histogram = (AsmHistogram) existingMetric;
                if (histogram.okToUpdate(end)) {
                    long elapsed = ((end - start) * TimeUtils.US_PER_MS) / count;
                    if (elapsed >= 0) {
                        histogram.update(elapsed);
                        histogram.setLastUpdateTime(end);
                    }
                }
            }
        }
    }

    public void update(final String streamId, final String name, final Number value, final MetricType metricType) {
        update(streamId, name, value, metricType, true);
    }

    public void update(final String streamId, final String name, final int count, final MetricType metricType) {
        if (count > 0)
            update(streamId, name, count, metricType, true);
    }

    public void send_tuple(String stream, int num_out_tasks) {
        if (JStormMetrics.enabled && num_out_tasks > 0) {
            update(stream, MetricDef.EMMITTED_NUM, num_out_tasks, MetricType.COUNTER);
            update(stream, MetricDef.SEND_TPS, num_out_tasks, MetricType.METER);
        }
    }

    public void recv_tuple(String component, String stream) {
        recv_tuple(component, stream, 1);
    }

    public void recv_tuple(String component, String stream, int tupleNum) {
        if (JStormMetrics.enabled) {
            update(stream, fastConcat(component, MetricDef.RECV_TPS), tupleNum, MetricType.METER);
        }
    }

    public void tupleLifeCycle(String component, String stream, long lifeCycleStart, long endTime, int count) {
        if (count > 0) {
            updateTime(stream, fastConcat(component, MetricDef.TUPLE_LIEF_CYCLE), lifeCycleStart, endTime, count, false);
        }
    }

    public void bolt_acked_tuple(String component, String stream) {
        bolt_acked_tuple(component, stream, 1);
    }

    public void bolt_acked_tuple(String component, String stream, int tupleNum) {
        if (JStormMetrics.enabled) {
            update(stream, MetricDef.ACKED_NUM, tupleNum, MetricType.COUNTER);
        }
    }

    public void update_bolt_acked_latency(String component, String stream, long latencyStart, long endTime) {
        update_bolt_acked_latency(component, stream, latencyStart, endTime, 1);
    }

    public void update_bolt_acked_latency(String component, String stream, long latencyStart, long endTime, int tupleNum) {
        if (JStormMetrics.enabled) {
            updateTime(stream, MetricDef.PROCESS_LATENCY, latencyStart, endTime, tupleNum, false);
        }
    }

    @SuppressWarnings("unused")
    public void bolt_failed_tuple(String component, String stream) {
        if (JStormMetrics.enabled) {
            update(stream, MetricDef.FAILED_NUM, 1, MetricType.COUNTER);
        }
    }

    public void boltFailedTuple(String component, String stream, int count) {
        if (JStormMetrics.enabled) {
            update(stream, MetricDef.FAILED_NUM, count, MetricType.COUNTER);
        }
    }

    public void spout_acked_tuple(String stream, long latencyStart, long lifeCycleStart, long endTime) {
        if (JStormMetrics.enabled) {
            update(stream, MetricDef.ACKED_NUM, 1, MetricType.COUNTER);
            updateTime(stream, MetricDef.PROCESS_LATENCY, latencyStart, endTime, false);
            updateTime(stream, fastConcat(Common.ACKER_COMPONENT_ID, MetricDef.TUPLE_LIEF_CYCLE), lifeCycleStart, endTime, false);
        }
    }

    public void spoutAckedTuple(String stream, int count) {
        if (JStormMetrics.enabled) {
            update(stream, MetricDef.ACKED_NUM, count, MetricType.COUNTER);
        }
    }

    private String fastConcat(String componentId, String metricName) {
        StringBuilder sb = new StringBuilder(32);
        return sb.append(componentId).append(".").append(metricName).toString();
    }

    public void spout_failed_tuple(String stream) {
        if (JStormMetrics.enabled) {
            update(stream, MetricDef.FAILED_NUM, 1, MetricType.COUNTER);
        }
    }

    public void spoutFailedTuple(String stream, int count) {
        if (JStormMetrics.enabled) {
            update(stream, MetricDef.FAILED_NUM, count, MetricType.COUNTER);
        }
    }

    public void spoutProcessLatency(String stream, long startTime, long endTime, int count) {
        if (JStormMetrics.enabled)
            updateTime(stream, MetricDef.PROCESS_LATENCY, startTime, endTime, count, false);
    }
}
