/**
 * 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.storm.hdfs.spout;

import java.util.Map;
import org.apache.storm.Config;
import org.apache.storm.StormSubmitter;
import org.apache.storm.generated.ClusterSummary;
import org.apache.storm.generated.ExecutorSummary;
import org.apache.storm.generated.KillOptions;
import org.apache.storm.generated.Nimbus;
import org.apache.storm.generated.SpoutStats;
import org.apache.storm.generated.TopologyInfo;
import org.apache.storm.generated.TopologySummary;
import org.apache.storm.metric.LoggingMetricsConsumer;
import org.apache.storm.task.OutputCollector;
import org.apache.storm.task.TopologyContext;
import org.apache.storm.topology.OutputFieldsDeclarer;
import org.apache.storm.topology.TopologyBuilder;
import org.apache.storm.topology.base.BaseRichBolt;
import org.apache.storm.tuple.Fields;
import org.apache.storm.tuple.Tuple;
import org.apache.storm.utils.NimbusClient;
import org.apache.storm.utils.Utils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;


public class HdfsSpoutTopology {

    public static final String SPOUT_ID = "hdfsspout";
    public static final String BOLT_ID = "constbolt";

    /**
     * Copies text file content from sourceDir to destinationDir. Moves source files into sourceDir after its done consuming
     */
    public static void main(String[] args) throws Exception {
        // 0 - validate args
        if (args.length < 7) {
            System.err.println("Please check command line arguments.");
            System.err.println("Usage :");
            System.err.println(
                HdfsSpoutTopology.class.toString() + " topologyName hdfsUri fileFormat sourceDir sourceArchiveDir badDir destinationDir.");
            System.err.println(" topologyName - topology name.");
            System.err.println(" hdfsUri - hdfs name node URI");
            System.err.println(" fileFormat -  Set to 'TEXT' for reading text files or 'SEQ' for sequence files.");
            System.err.println(" sourceDir  - read files from this HDFS dir using HdfsSpout.");
            System.err.println(" archiveDir - after a file in sourceDir is read completely, it is moved to this HDFS location.");
            System.err.println(" badDir - files that cannot be read properly will be moved to this HDFS location.");
            System.err.println(" spoutCount - Num of spout instances.");
            System.err.println();
            System.exit(-1);
        }

        // 1 - parse cmd line args
        String topologyName = args[0];
        String hdfsUri = args[1];
        String fileFormat = args[2];
        String sourceDir = args[3];
        String archiveDir = args[4];
        String badDir = args[5];
        int spoutNum = Integer.parseInt(args[6]);

        // 2 - create and configure spout and bolt
        ConstBolt bolt = new ConstBolt();

        HdfsSpout spout = new HdfsSpout().withOutputFields(TextFileReader.defaultFields)
                                         .setReaderType(fileFormat)
                                         .setHdfsUri(hdfsUri)
                                         .setSourceDir(sourceDir)
                                         .setArchiveDir(archiveDir)
                                         .setBadFilesDir(badDir);

        // 3 - Create and configure topology
        Config conf = new Config();
        conf.setNumWorkers(1);
        conf.setNumAckers(1);
        conf.setMaxTaskParallelism(1);
        conf.setDebug(true);
        conf.registerMetricsConsumer(LoggingMetricsConsumer.class);

        TopologyBuilder builder = new TopologyBuilder();
        builder.setSpout(SPOUT_ID, spout, spoutNum);
        builder.setBolt(BOLT_ID, bolt, 1).shuffleGrouping(SPOUT_ID);

        // 4 - submit topology, wait for a few min and terminate it
        Map<String, Object> clusterConf = Utils.readStormConfig();
        StormSubmitter.submitTopologyWithProgressBar(topologyName, conf, builder.createTopology());
        Nimbus.Iface client = NimbusClient.getConfiguredClient(clusterConf).getClient();

        // 5 - Print metrics every 30 sec, kill topology after 20 min
        for (int i = 0; i < 40; i++) {
            Thread.sleep(30 * 1000);
            printMetrics(client, topologyName);
        }
        kill(client, topologyName);
    } // main

    private static void kill(Nimbus.Iface client, String topologyName) throws Exception {
        KillOptions opts = new KillOptions();
        opts.set_wait_secs(0);
        client.killTopologyWithOpts(topologyName, opts);
    }

    static void printMetrics(Nimbus.Iface client, String name) throws Exception {
        ClusterSummary summary = client.getClusterInfo();
        String id = null;
        for (TopologySummary ts : summary.get_topologies()) {
            if (name.equals(ts.get_name())) {
                id = ts.get_id();
            }
        }
        if (id == null) {
            throw new Exception("Could not find a topology named " + name);
        }
        TopologyInfo info = client.getTopologyInfo(id);
        int uptime = info.get_uptime_secs();
        long acked = 0;
        long failed = 0;
        double weightedAvgTotal = 0.0;
        for (ExecutorSummary exec : info.get_executors()) {
            if ("spout".equals(exec.get_component_id())) {
                SpoutStats stats = exec.get_stats().get_specific().get_spout();
                Map<String, Long> failedMap = stats.get_failed().get(":all-time");
                Map<String, Long> ackedMap = stats.get_acked().get(":all-time");
                Map<String, Double> avgLatMap = stats.get_complete_ms_avg().get(":all-time");
                for (String key : ackedMap.keySet()) {
                    if (failedMap != null) {
                        Long tmp = failedMap.get(key);
                        if (tmp != null) {
                            failed += tmp;
                        }
                    }
                    long ackVal = ackedMap.get(key);
                    double latVal = avgLatMap.get(key) * ackVal;
                    acked += ackVal;
                    weightedAvgTotal += latVal;
                }
            }
        }
        double avgLatency = weightedAvgTotal / acked;
        System.out.println("uptime: " + uptime + " acked: " + acked + " avgLatency: " + avgLatency + " acked/sec: " +
                           (((double) acked) / uptime + " failed: " + failed));
    }

    public static class ConstBolt extends BaseRichBolt {
        public static final String FIELDS = "message";
        private static final long serialVersionUID = -5313598399155365865L;
        private static final Logger log = LoggerFactory.getLogger(ConstBolt.class);
        int count = 0;
        private OutputCollector collector;

        public ConstBolt() {
        }

        @Override
        public void prepare(Map<String, Object> conf, TopologyContext context, OutputCollector collector) {
            this.collector = collector;
        }

        @Override
        public void execute(Tuple tuple) {
            log.info("Received tuple : {}", tuple.getValue(0));
            count++;
            if (count == 3) {
                collector.fail(tuple);
            } else {
                collector.ack(tuple);
            }
        }

        @Override
        public void declareOutputFields(OutputFieldsDeclarer declarer) {
            declarer.declare(new Fields(FIELDS));
        }
    } // class
}
