#!/usr/bin/python
# Copyright 2013 Google Inc. All Rights Reserved.
# Licensed 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.

"""Continuous TV-related statistics monitor.

Like iostat/vmstat, but for TV.
"""

__author__ = 'dgentry@google.com (Denton Gentry)'

import json
import re
import socket
import struct
import sys
import time
import options


optspec = """
tvstat [options...] <interval>
--
"""


# unit tests can override these with fake versions
PLATFORM = '/etc/platform'
PROC_NET_DEV = '/proc/net/dev'
PROC_NET_UDP = '/proc/net/udp'
DC_JSON = '/tmp/cwmp/monitoring/dejittering/tr_135_total_decoderstats%d.json'
DJ_JSON = '/tmp/cwmp/monitoring/dejittering/tr_135_total_djstats%d.json'
TS_JSON = '/tmp/cwmp/monitoring/ts/tr_135_total_tsstats%d.json'


# Field numbers in /proc/net/dev
RX_ERRS = 3
RX_DROP = 4
RX_FIFO = 5
RX_FRAME = 6


def GetIfErrors(dev):
  """Return (rxerr, rxdrop, rxfifo, rxframe) for network interface named dev."""
  for line in open(PROC_NET_DEV):
    f = line.split()
    dev_colon = dev + ':'
    if f[0] == dev_colon:
      return (int(f[RX_ERRS]), int(f[RX_DROP]),
              int(f[RX_FIFO]), int(f[RX_FRAME]))
  return (0, 0, 0, 0)


def UnpackAlanCoxIP(packed):
  """Convert hex IP addresses to strings.

  /proc/net/igmp and /proc/net/udp both contain IP addresses printed as
  a hex string, _without_ calling ntohl() first.

  Example from /proc/net/udp on a little endian machine:
  sl  local_address rem_address   st tx_queue rx_queue ...
  464: 010002E1:07D0 00000000:0000 07 00000000:00000000 ...

  On a big-endian machine:
  sl  local_address rem_address   st tx_queue rx_queue ...
  464: E1020001:07D0 00000000:0000 07 00000000:00000000 ...

  Args:
    packed: the hex thingy.
  Returns:
    A conventional dotted quad IP address encoding.
  """
  return socket.inet_ntop(socket.AF_INET, struct.pack('=L', int(packed, 16)))


def ParseProcNetUDP():
  """Parse /proc/net/udp, returns dict of drops for all entries.

    sl  local_address rem_address   st tx_queue rx_queue tr tm->when retrnsmt
    464: 010002E1:07D0 00000000:0000 07 00000000:00000000 00:00000000 00000000

    uid  timeout inode ref pointer drops
    0        0 6187 2 b1f64e00 0

  Returns:
    a dict indexed by ip:port of the number of dropped packets in UDP.
  """
  udp = dict()
  with open(PROC_NET_UDP) as f:
    for line in f:
      try:
        line = ' '.join(line.split())
        fields = re.split('[ :]', line)
        ip = UnpackAlanCoxIP(fields[2])
        port = int(fields[3], 16)
        key = '%s:%d' % (ip, port)
        current = udp.get(key, 0)
        udp[key] = current + int(fields[17])
      except (ValueError, IndexError):
        # comment line, or something
        continue
  return udp


def GetUDPDrops():
  """Return UDP Drops for all streams."""
  udp = ParseProcNetUDP()
  drops = 0
  for i in range(1, 9):
    try:
      d = json.load(open(TS_JSON % i))
      mc = d['STBService'][0]['MainStream'][0]['MulticastStats']
      ip = mc['MulticastGroup']
      drops += int(udp[ip])
    except (IOError, KeyError, IndexError):
      continue
  return drops


def GetTsErrors():
  """Return Errors from sagesrv JSON files.

  Returns:
    (PacketDiscontinuityCount, DropBytes, DropPackets, PacketErrorCount)
    for all existing MainStreams.
  """
  errs = {
      'PacketDiscontinuityCounter': 0, 'DropBytes': 0,
      'DropPackets': 0, 'PacketErrorCount': 0,
  }
  for i in range(1, 9):
    try:
      d = json.load(open(TS_JSON % i))
      mpeg = d['STBService'][0]['MainStream'][0]['MPEG2TSStats']
    except IOError:
      continue
    pdc = int(mpeg.get('PacketDiscontinuityCounter', 0))
    errs['PacketDiscontinuityCounter'] += pdc
    errs['DropBytes'] += int(mpeg.get('DropBytes', 0))
    errs['DropPackets'] += int(mpeg.get('DropPackets', 0))
    errs['PacketErrorCount'] += int(mpeg.get('PacketErrorCount', 0))
  return errs


def GetVideoHardwareErrors():
  """Return Errors from miniclient JSON files for the Video hardware.

  Returns:
    a dict populated with stats from the hardware pipelines.
  """
  errs = {
      'VideoDecodeErrors': 0, 'DecodeOverflows': 0, 'DecodeDrops': 0,
      'DisplayErrors': 0, 'DisplayDrops': 0, 'DisplayUnderflows': 0,
      'VideoWatchdogs': 0, 'AudioDecodeErrors': 0, 'AudioFifoOverflows': 0,
      'AudioFifoUnderflows': 0, 'AudioWatchdogs': 0, 'AVPtsDifference' : 0
  }
  for i in range(1, 9):
    try:
      d = json.load(open(DC_JSON % i))
      decode = d['STBService'][0]['MainStream'][0]['DecoderStats']
    except IOError:
      continue
    errs['VideoDecodeErrors'] += int(decode.get('VideoDecodeErrors', 0))
    errs['DecodeOverflows'] += int(decode.get('DecodeOverflows', 0))
    errs['DecodeDrops'] += int(decode.get('DecodeDrops', 0))
    errs['DisplayErrors'] += int(decode.get('DisplayErrors', 0))
    errs['DisplayDrops'] += int(decode.get('DisplayDrops', 0))
    errs['DisplayUnderflows'] += int(decode.get('DisplayUnderflows', 0))
    errs['VideoWatchdogs'] += int(decode.get('VideoWatchdogs', 0))
    errs['AudioDecodeErrors'] += int(decode.get('AudioDecodeErrors', 0))
    errs['AudioFifoOverflows'] += int(decode.get('AudioFifoOverflows', 0))
    errs['AudioFifoUnderflows'] += int(decode.get('AudioFifoUnderflows', 0))
    errs['AudioWatchdogs'] += int(decode.get('AudioWatchdogs', 0))
    audio_pts = int(decode.get('AudioPts', 0))
    video_pts = int(decode.get('VideoPts', 0))
    if audio_pts and video_pts:
      errs['AVPtsDifference'] = audio_pts - video_pts
  return errs


def GetVideoSoftwareErrors():
  """Return Errors from miniclient JSON files for software errors.

  Returns:
    a dict populated with Overrruns, Underruns, and EmptyBufferTime.
  """
  errs = {
      'Overrruns': 0, 'Underruns': 0, 'EmptyBufferTime': 0
  }
  for i in range(1, 9):
    try:
      d = json.load(open(DJ_JSON % i))
      decode = d['STBService'][0]['MainStream'][0]['DejitteringStats']
    except IOError:
      continue
    errs['Overrruns'] += int(decode.get('Overrruns', 0))
    errs['Underruns'] += int(decode.get('Underruns', 0))
    errs['EmptyBufferTime'] += int(decode.get('EmptyBufferTime', 0))
  return errs



def TvBoxStats(old):
  new = dict()
  new.update(GetVideoHardwareErrors())
  new.update(GetVideoSoftwareErrors())
  print '%6d %5d %5d %5d  %5d %5d %5d %5d %5d %6d %4d %4d' % (
      Delta(new, old, 'VideoDecodeErrors'),
      Delta(new, old, 'DecodeOverflows'),
      Delta(new, old, 'DecodeDrops'),
      Delta(new, old, 'AudioDecodeErrors'),
      Delta(new, old, 'DisplayErrors'),
      Delta(new, old, 'DisplayDrops'),
      Delta(new, old, 'DisplayUnderflows'),
      Delta(new, old, 'AudioFifoOverflows'),
      Delta(new, old, 'AudioFifoUnderflows'),
      Delta(new, old, 'AVPtsDifference'),
      Delta(new, old, 'VideoWatchdogs'),
      Delta(new, old, 'AudioWatchdogs'))
  return new


def StorageBoxStats(old, wan_if):
  new = dict()
  (new['wan_rxerr'], new['wan_rxdrop'],
   new['wan_rxfifo'], new['wan_rxframe']) = GetIfErrors(wan_if)
  (new['br0_rxerr'], new['br0_rxdrop'],
   new['br0_rxfifo'], new['br0_rxframe']) = GetIfErrors('br0')
  new.update(GetTsErrors())
  new['UdpDrops'] = GetUDPDrops()
  print '%7d %5d %5d %6d %7d %4d %4d %4d %5d %4d %4d %4d %5d' % (
      Delta(new, old, 'PacketDiscontinuityCounter'),
      Delta(new, old, 'DropBytes'),
      Delta(new, old, 'DropPackets'),
      Delta(new, old, 'PacketErrorCount'),
      Delta(new, old, 'UdpDrops'),
      Delta(new, old, 'wan_rxerr'),
      Delta(new, old, 'wan_rxdrop'),
      Delta(new, old, 'wan_rxfifo'),
      Delta(new, old, 'wan_rxframe'),
      Delta(new, old, 'br0_rxerr'),
      Delta(new, old, 'br0_rxdrop'),
      Delta(new, old, 'br0_rxfifo'),
      Delta(new, old, 'br0_rxframe'))
  return new


def Delta(new, old, field):
  """Return difference new - old for field."""
  if field in old:
    return new[field] - old[field]
  else:
    return new[field]


def main():
  o = options.Options(optspec)
  _, _, extra = o.parse(sys.argv[1:])

  interval = int(extra[0]) if len(extra) else -1

  platform = open(PLATFORM).read()
  tvbox = True if 'GFHD' in platform else False
  wan_if = 'wan0' if 'GFRG2' in platform else 'eth0'

  new = dict()
  old = dict()

  if tvbox:
    print '--------decoder---------- ------------------display---------------------'
    print 'verror vover vdrop aerror  verr vdrop vundr aover aundr a2vpts vwch awch'
  else:
    print '---------sagesrv------------------ --------%s-------- --------br0---------' % wan_if
    print ' discon dropb dropp pkterr udpdrop  err drop fifo frame  err drop fifo frame'

  while True:
    if tvbox:
      new = TvBoxStats(old)
    else:
      new = StorageBoxStats(old, wan_if)

    if interval <= 0:
      return 0

    time.sleep(interval)
    old = new

  return 0


if __name__ == '__main__':
  sys.exit(main())
