/*
 * 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.druid.java.util.metrics;

import com.google.common.base.Joiner;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import org.apache.druid.java.util.common.logger.Logger;
import org.apache.druid.java.util.emitter.service.ServiceEmitter;
import org.apache.druid.java.util.emitter.service.ServiceMetricEvent;
import org.hyperic.sigar.Cpu;
import org.hyperic.sigar.DirUsage;
import org.hyperic.sigar.DiskUsage;
import org.hyperic.sigar.FileSystem;
import org.hyperic.sigar.FileSystemUsage;
import org.hyperic.sigar.Mem;
import org.hyperic.sigar.NetInterfaceConfig;
import org.hyperic.sigar.NetInterfaceStat;
import org.hyperic.sigar.NetStat;
import org.hyperic.sigar.Sigar;
import org.hyperic.sigar.SigarException;
import org.hyperic.sigar.Swap;
import org.hyperic.sigar.Tcp;
import org.hyperic.sigar.Uptime;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;

public class SysMonitor extends FeedDefiningMonitor
{
  private static final Logger log = new Logger(SysMonitor.class);

  private final Sigar sigar = SigarUtil.getSigar();

  private final List<String> fsTypeWhitelist = ImmutableList.of("local");
  private final List<String> netAddressBlacklist = ImmutableList.of("0.0.0.0", "127.0.0.1");

  private final List<Stats> statsList;

  private Map<String, String[]> dimensions;

  public SysMonitor()
  {
    this(ImmutableMap.of());
  }

  public SysMonitor(Map<String, String[]> dimensions)
  {
    this(dimensions, DEFAULT_METRICS_FEED);
  }

  public SysMonitor(Map<String, String[]> dimensions, String feed)
  {
    super(feed);
    Preconditions.checkNotNull(dimensions);
    this.dimensions = ImmutableMap.copyOf(dimensions);

    sigar.enableLogging(true);

    this.statsList = new ArrayList<Stats>();
    this.statsList.addAll(
        Arrays.asList(
            new MemStats(),
            new FsStats(),
            new DiskStats(),
            new NetStats(),
            new CpuStats(),
            new SwapStats(),
            new SysStats(),
            new TcpStats()
        )
    );
  }

  public void addDirectoriesToMonitor(String[] dirList)
  {
    for (int i = 0; i < dirList.length; i++) {
      dirList[i] = dirList[i].trim();
    }
    statsList.add(new DirStats(dirList));
  }

  @Override
  public boolean doMonitor(ServiceEmitter emitter)
  {
    for (Stats stats : statsList) {
      stats.emit(emitter);
    }
    return true;
  }

  private interface Stats
  {
    void emit(ServiceEmitter emitter);
  }

  private class MemStats implements Stats
  {
    @Override
    public void emit(ServiceEmitter emitter)
    {
      Mem mem = null;
      try {
        mem = sigar.getMem();
      }
      catch (SigarException e) {
        log.error(e, "Failed to get Mem");
      }
      if (mem != null) {
        final Map<String, Long> stats = ImmutableMap.of(
            "sys/mem/max", mem.getTotal(),
            "sys/mem/used", mem.getUsed(),
            "sys/mem/actual/used", mem.getActualUsed(),
            "sys/mem/actual/free", mem.getActualFree()
        );
        final ServiceMetricEvent.Builder builder = builder();
        MonitorUtils.addDimensionsToBuilder(builder, dimensions);
        for (Map.Entry<String, Long> entry : stats.entrySet()) {
          emitter.emit(builder.build(entry.getKey(), entry.getValue()));
        }
      }
    }
  }

  /**
   * Gets the swap stats from sigar and emits the periodic pages in & pages out of memory
   * along with the max swap and free swap memory.
   */
  private class SwapStats implements Stats
  {
    private long prevPageIn = 0, prevPageOut = 0;

    private SwapStats()
    {
      try {
        Swap swap = sigar.getSwap();
        this.prevPageIn = swap.getPageIn();
        this.prevPageOut = swap.getPageOut();
      }
      catch (SigarException e) {
        log.error(e, "Failed to get Swap");
      }
    }

    @Override
    public void emit(ServiceEmitter emitter)
    {
      Swap swap = null;
      try {
        swap = sigar.getSwap();
      }
      catch (SigarException e) {
        log.error(e, "Failed to get Swap");
      }
      if (swap != null) {
        long currPageIn = swap.getPageIn();
        long currPageOut = swap.getPageOut();

        final Map<String, Long> stats = ImmutableMap.of(
            "sys/swap/pageIn", (currPageIn - prevPageIn),
            "sys/swap/pageOut", (currPageOut - prevPageOut),
            "sys/swap/max", swap.getTotal(),
            "sys/swap/free", swap.getFree()
        );

        final ServiceMetricEvent.Builder builder = builder();
        MonitorUtils.addDimensionsToBuilder(builder, dimensions);
        for (Map.Entry<String, Long> entry : stats.entrySet()) {
          emitter.emit(builder.build(entry.getKey(), entry.getValue()));
        }

        this.prevPageIn = currPageIn;
        this.prevPageOut = currPageOut;
      }
    }
  }

  /**
   * Gets the disk usage of a particular directory.
   */
  private class DirStats implements Stats
  {
    private final String[] dirList;

    private DirStats(String[] dirList)
    {
      this.dirList = dirList;
    }

    @Override
    public void emit(ServiceEmitter emitter)
    {
      for (String dir : dirList) {
        DirUsage du = null;
        try {
          du = sigar.getDirUsage(dir);
        }
        catch (SigarException e) {
          log.error("Failed to get DiskUsage for [%s] due to   [%s]", dir, e.getMessage());
        }
        if (du != null) {
          final Map<String, Long> stats = ImmutableMap.of(
              "sys/storage/used", du.getDiskUsage()
          );
          final ServiceMetricEvent.Builder builder = builder()
              .setDimension("fsDirName", dir); // fsDirName because FsStats uses fsDirName
          MonitorUtils.addDimensionsToBuilder(builder, dimensions);
          for (Map.Entry<String, Long> entry : stats.entrySet()) {
            emitter.emit(builder.build(entry.getKey(), entry.getValue()));
          }
        }
      }
    }
  }

  private class FsStats implements Stats
  {
    @Override
    public void emit(ServiceEmitter emitter)
    {
      FileSystem[] fss = null;
      try {
        fss = sigar.getFileSystemList();
      }
      catch (SigarException e) {
        log.error(e, "Failed to get FileSystem list");
      }
      if (fss != null) {
        log.debug("Found FileSystem list: [%s]", Joiner.on(", ").join(fss));
        for (FileSystem fs : fss) {
          final String name = fs.getDirName(); // (fs.getDevName() does something wonky here!)
          if (fsTypeWhitelist.contains(fs.getTypeName())) {
            FileSystemUsage fsu = null;
            try {
              fsu = sigar.getFileSystemUsage(name);
            }
            catch (SigarException e) {
              log.error(e, "Failed to get FileSystemUsage[%s]", name);
            }
            if (fsu != null) {
              final Map<String, Long> stats = ImmutableMap.<String, Long>builder()
                  .put("sys/fs/max", fsu.getTotal() * 1024)
                  .put("sys/fs/used", fsu.getUsed() * 1024)
                  .put("sys/fs/files/count", fsu.getFiles())
                  .put("sys/fs/files/free", fsu.getFreeFiles())
                  .build();
              final ServiceMetricEvent.Builder builder = builder()
                  .setDimension("fsDevName", fs.getDevName())
                  .setDimension("fsDirName", fs.getDirName())
                  .setDimension("fsTypeName", fs.getTypeName())
                  .setDimension("fsSysTypeName", fs.getSysTypeName())
                  .setDimension("fsOptions", fs.getOptions().split(","));
              MonitorUtils.addDimensionsToBuilder(builder, dimensions);
              for (Map.Entry<String, Long> entry : stats.entrySet()) {
                emitter.emit(builder.build(entry.getKey(), entry.getValue()));
              }
            }
          } else {
            log.debug("Not monitoring fs stats for name[%s] with typeName[%s]", name, fs.getTypeName());
          }
        }
      }
    }
  }

  private class DiskStats implements Stats
  {
    private final KeyedDiff diff = new KeyedDiff();

    @Override
    public void emit(ServiceEmitter emitter)
    {
      FileSystem[] fss = null;
      try {
        fss = sigar.getFileSystemList();
      }
      catch (SigarException e) {
        log.error(e, "Failed to get FileSystem list");
      }
      if (fss != null) {
        log.debug("Found FileSystem list: [%s]", Joiner.on(", ").join(fss));
        for (FileSystem fs : fss) {
          // fs.getDevName() appears to give the same results here, but on some nodes results for one disc were substituted by another
          // LOG: Sigar - /proc/diskstats /dev/xvdj -> /dev/xvdb [202,16]
          final String name = fs.getDirName();
          if (fsTypeWhitelist.contains(fs.getTypeName())) {
            DiskUsage du = null;
            try {
              du = sigar.getDiskUsage(name);
            }
            catch (SigarException e) {
              log.error(e, "Failed to get DiskUsage[%s]", name);
            }
            if (du != null) {
              final Map<String, Long> stats = diff.to(
                  name, ImmutableMap.<String, Long>builder()
                      .put("sys/disk/read/size", du.getReadBytes())
                      .put("sys/disk/read/count", du.getReads())
                      .put("sys/disk/write/size", du.getWriteBytes())
                      .put("sys/disk/write/count", du.getWrites())
                      .put("sys/disk/queue", Double.valueOf(du.getQueue()).longValue())
                      .put("sys/disk/serviceTime", Double.valueOf(du.getServiceTime()).longValue())
                      .build()
              );
              log.debug("DiskUsage diff for [%s]: %s", name, stats);
              if (stats != null) {
                final ServiceMetricEvent.Builder builder = builder()
                    .setDimension("fsDevName", fs.getDevName())
                    .setDimension("fsDirName", fs.getDirName())
                    .setDimension("fsTypeName", fs.getTypeName())
                    .setDimension("fsSysTypeName", fs.getSysTypeName())
                    .setDimension("fsOptions", fs.getOptions().split(","));
                MonitorUtils.addDimensionsToBuilder(builder, dimensions);
                for (Map.Entry<String, Long> entry : stats.entrySet()) {
                  emitter.emit(builder.build(entry.getKey(), entry.getValue()));
                }
              }
            }
          } else {
            log.debug("Not monitoring disk stats for name[%s] with typeName[%s]", name, fs.getTypeName());
          }
        }
      }
    }
  }

  private class NetStats implements Stats
  {
    private final KeyedDiff diff = new KeyedDiff();

    @Override
    public void emit(ServiceEmitter emitter)
    {
      String[] ifaces = null;
      try {
        ifaces = sigar.getNetInterfaceList();
      }
      catch (SigarException e) {
        log.error(e, "Failed to get NetInterface list");
      }
      if (ifaces != null) {
        log.debug("Found NetInterface list: [%s]", Joiner.on(", ").join(ifaces));
        for (String name : ifaces) {
          NetInterfaceConfig netconf = null;
          try {
            netconf = sigar.getNetInterfaceConfig(name);
          }
          catch (SigarException e) {
            log.error(e, "Failed to get NetInterfaceConfig[%s]", name);
          }
          if (netconf != null) {
            if (!(netAddressBlacklist.contains(netconf.getAddress()))) {
              NetInterfaceStat netstat = null;
              try {
                netstat = sigar.getNetInterfaceStat(name);
              }
              catch (SigarException e) {
                log.error(e, "Failed to get NetInterfaceStat[%s]", name);
              }
              if (netstat != null) {
                final Map<String, Long> stats = diff.to(
                    name, ImmutableMap.<String, Long>builder()
                        .put("sys/net/read/size", netstat.getRxBytes())
                        .put("sys/net/read/packets", netstat.getRxPackets())
                        .put("sys/net/read/errors", netstat.getRxErrors())
                        .put("sys/net/read/dropped", netstat.getRxDropped())
                        .put("sys/net/read/overruns", netstat.getRxOverruns())
                        .put("sys/net/read/frame", netstat.getRxFrame())
                        .put("sys/net/write/size", netstat.getTxBytes())
                        .put("sys/net/write/packets", netstat.getTxPackets())
                        .put("sys/net/write/errors", netstat.getTxErrors())
                        .put("sys/net/write/dropped", netstat.getTxDropped())
                        .put("sys/net/write/collisions", netstat.getTxCollisions())
                        .put("sys/net/write/overruns", netstat.getTxOverruns())
                        .build()
                );
                if (stats != null) {
                  final ServiceMetricEvent.Builder builder = builder()
                      .setDimension("netName", netconf.getName())
                      .setDimension("netAddress", netconf.getAddress())
                      .setDimension("netHwaddr", netconf.getHwaddr());
                  MonitorUtils.addDimensionsToBuilder(builder, dimensions);
                  for (Map.Entry<String, Long> entry : stats.entrySet()) {
                    emitter.emit(builder.build(entry.getKey(), entry.getValue()));
                  }
                }
              }
            } else {
              log.debug("Not monitoring net stats for name[%s] with address[%s]", name, netconf.getAddress());
            }
          }
        }
      }
    }
  }

  private class CpuStats implements Stats
  {
    private final KeyedDiff diff = new KeyedDiff();

    @Override
    public void emit(ServiceEmitter emitter)
    {
      Cpu[] cpus = null;
      try {
        cpus = sigar.getCpuList();
      }
      catch (SigarException e) {
        log.error(e, "Failed to get Cpu list");
      }
      if (cpus != null) {
        log.debug("Found Cpu list: [%s]", Joiner.on(", ").join(cpus));
        for (int i = 0; i < cpus.length; ++i) {
          final Cpu cpu = cpus[i];
          final String name = Integer.toString(i);
          final Map<String, Long> stats = diff.to(
              name, ImmutableMap.<String, Long>builder()
                  .put("user", cpu.getUser()) // user = Δuser / Δtotal
                  .put("sys", cpu.getSys()) // sys = Δsys / Δtotal
                  .put("nice", cpu.getNice()) // nice = Δnice / Δtotal
                  .put("wait", cpu.getWait()) // wait = Δwait / Δtotal
                  .put("irq", cpu.getIrq()) // irq = Δirq / Δtotal
                  .put("softIrq", cpu.getSoftIrq()) // softIrq = ΔsoftIrq / Δtotal
                  .put("stolen", cpu.getStolen()) // stolen = Δstolen / Δtotal
                  .put("_total", cpu.getTotal()) // (not reported)
                  .build()
          );
          if (stats != null) {
            final long total = stats.remove("_total");
            for (Map.Entry<String, Long> entry : stats.entrySet()) {
              final ServiceMetricEvent.Builder builder = builder()
                  .setDimension("cpuName", name)
                  .setDimension("cpuTime", entry.getKey());
              MonitorUtils.addDimensionsToBuilder(builder, dimensions);
              emitter.emit(builder.build("sys/cpu", entry.getValue() * 100 / total)); // [0,100]
            }
          }
        }
      }
    }
  }

  private class SysStats implements Stats
  {
    @Override
    public void emit(ServiceEmitter emitter)
    {
      final ServiceMetricEvent.Builder builder = builder();
      MonitorUtils.addDimensionsToBuilder(builder, dimensions);

      Uptime uptime = null;
      try {
        uptime = sigar.getUptime();
      }
      catch (SigarException e) {
        log.error(e, "Failed to get Uptime");
      }

      double[] la = null;
      try {
        la = sigar.getLoadAverage();
      }
      catch (SigarException e) {
        log.error(e, "Failed to get Load Average");
      }

      if (uptime != null) {
        final Map<String, Number> stats = ImmutableMap.of(
            "sys/uptime", Double.valueOf(uptime.getUptime()).longValue()
        );
        for (Map.Entry<String, Number> entry : stats.entrySet()) {
          emitter.emit(builder.build(entry.getKey(), entry.getValue()));
        }
      }

      if (la != null) {
        final Map<String, Number> stats = ImmutableMap.of(
            "sys/la/1", la[0],
            "sys/la/5", la[1],
            "sys/la/15", la[2]
        );
        for (Map.Entry<String, Number> entry : stats.entrySet()) {
          emitter.emit(builder.build(entry.getKey(), entry.getValue()));
        }
      }
    }
  }

  private class TcpStats implements Stats
  {
    private final KeyedDiff diff = new KeyedDiff();

    @Override
    public void emit(ServiceEmitter emitter)
    {
      final ServiceMetricEvent.Builder builder = builder();
      MonitorUtils.addDimensionsToBuilder(builder, dimensions);

      Tcp tcp = null;
      try {
        tcp = sigar.getTcp();
      }
      catch (SigarException e) {
        log.error(e, "Failed to get Tcp");
      }

      if (tcp != null) {
        final Map<String, Long> stats = diff.to(
            "tcp", ImmutableMap.<String, Long>builder()
                .put("sys/tcp/activeOpens", tcp.getActiveOpens())
                .put("sys/tcp/passiveOpens", tcp.getPassiveOpens())
                .put("sys/tcp/attemptFails", tcp.getAttemptFails())
                .put("sys/tcp/estabResets", tcp.getEstabResets())
                .put("sys/tcp/in/segs", tcp.getInSegs())
                .put("sys/tcp/in/errs", tcp.getInErrs())
                .put("sys/tcp/out/segs", tcp.getOutSegs())
                .put("sys/tcp/out/rsts", tcp.getOutRsts())
                .put("sys/tcp/retrans/segs", tcp.getRetransSegs())
                .build()
        );
        if (stats != null) {
          for (Map.Entry<String, Long> entry : stats.entrySet()) {
            emitter.emit(builder.build(entry.getKey(), entry.getValue()));
          }
        }
      }

      NetStat netStat = null;
      try {
        netStat = sigar.getNetStat();
      }
      catch (SigarException e) {
        log.error(e, "Failed to get NetStat");
      }
      if (netStat != null) {
        final Map<String, Long> stats = ImmutableMap.<String, Long>builder()
            .put("sys/net/inbound", (long) netStat.getAllInboundTotal())
            .put("sys/net/outbound", (long) netStat.getAllOutboundTotal())
            .put("sys/tcp/inbound", (long) netStat.getTcpInboundTotal())
            .put("sys/tcp/outbound", (long) netStat.getTcpOutboundTotal())
            .put(
                "sys/tcp/state/established",
                (long) netStat.getTcpEstablished()
            )
            .put("sys/tcp/state/synSent", (long) netStat.getTcpSynSent())
            .put("sys/tcp/state/synRecv", (long) netStat.getTcpSynRecv())
            .put("sys/tcp/state/finWait1", (long) netStat.getTcpFinWait1())
            .put("sys/tcp/state/finWait2", (long) netStat.getTcpFinWait2())
            .put("sys/tcp/state/timeWait", (long) netStat.getTcpTimeWait())
            .put("sys/tcp/state/close", (long) netStat.getTcpClose())
            .put("sys/tcp/state/closeWait", (long) netStat.getTcpCloseWait())
            .put("sys/tcp/state/lastAck", (long) netStat.getTcpLastAck())
            .put("sys/tcp/state/listen", (long) netStat.getTcpListen())
            .put("sys/tcp/state/closing", (long) netStat.getTcpClosing())
            .put("sys/tcp/state/idle", (long) netStat.getTcpIdle())
            .put("sys/tcp/state/bound", (long) netStat.getTcpBound())
            .build();
        for (Map.Entry<String, Long> entry : stats.entrySet()) {
          emitter.emit(builder.build(entry.getKey(), entry.getValue()));
        }
      }
    }
  }
}
