#!/usr/bin/python3

import sys
import subprocess
import time
import socket
import argparse
import operator
from multiprocessing import Process, Queue

def first_ping_test(IP, IP_family, pingoptions):
    # Used only once at start mainly to check if the ping options work fine
    # This is the only 'ping' call where the error message from 'ping' is printed out.
    if IP_family == socket.AF_INET6:
        pingcmd='ping6' # IPv6
    else:
        pingcmd='ping'  # IPv4
    Ping=subprocess.Popen([ pingcmd, "-c", "1", "-n", IP ] + pingoptions,
                              stdout = subprocess.PIPE,
                              stderr = subprocess.PIPE)
    pingstdout, pingstderr = Ping.communicate()
    Ping.wait()
    # man ping:
    # If ping does not receive any reply packets at all it will exit with code
    # 1. If a packet count and deadline are both specified, and  fewer  than
    # count packets are received by the time the deadline has arrived, it will
    # also exit with code 1.  On other error it exits with code 2. Otherwise it
    # exits with code 0. This makes it possible to use the exit code to see if
    # a host is alive or not.
    if Ping.returncode == 2:
        print(pingstdout.decode("utf-8"))
        print(pingstderr.decode("utf-8"), file=sys.stderr)
        return False
    return True

def one_packet_ping(sleep, IP, IP_family, pingresults, pingoptions):
    if IP_family == socket.AF_INET6:
        pingcmd='ping6' # IPv6
    else:
        pingcmd='ping'  # IPv4
    try:
        import time
        time.sleep(sleep)
        Ping=subprocess.Popen([ pingcmd, "-c", "1", "-n", IP ] + pingoptions,
                              stdout = subprocess.PIPE,
                              stderr = subprocess.PIPE)
        pingstdout, pingstderr = Ping.communicate()
        Ping.wait()
        time=0
        if Ping.returncode==0:
            ping_stdout_lastline = str(pingstdout).split('\\n')[-2]
            start=ping_stdout_lastline.find('=')+1
            end=start+ping_stdout_lastline[start:].find('/')
            time=float(ping_stdout_lastline[start:end])
        pingresults.put([Ping.returncode,time])
    except KeyboardInterrupt:
        return

def print_header(pingprobes):
    print(f"Time         x=lost   .=OK  { ' '.ljust(pingprobes-20) }loss(%)    min [ms]    avg [ms]    max [ms]")

def one_minute_pings(pingprobes, IP, IP_family, min_time, max_time, all_time, all_downtime, pingoptions, first):
    def print_stat(pingprobes, printed, pingok, packet_loss):
        if printed > 0:
            space_needed=pingprobes-printed
            print('  '.ljust(space_needed), end='')
            packet_loss=round((100*packet_loss)/printed, 1)
            print(f"{ str(packet_loss).rjust(4) }%", end='')
            if packet_loss != 100:
                print(f"{ ' '.ljust(6) }{ str(l_min_time)[:6].ljust(6) }{ ' '.ljust(6) }{ str(l_all_time/pingok)[:6].ljust(6) }{ ' '.ljust(6) }{ str(l_max_time)[:6].ljust(6) }")
            else:
                print('')

    try:
        printed=0
        pingok=0
        packet_loss=0
        l_min_time=1000000.0
        l_max_time=0.0
        l_all_time=0
        pingresults=Queue()

        current=time.strftime('%H%M')
        print(f"{ time.strftime('%H:%M') }: ", end='')
        startsec = 0
        realstartsec = int(time.strftime('%S'))
        if first:
            startsec = realstartsec
            skippedpings =  int (pingprobes * startsec / 60 )
            pingprobes = pingprobes - skippedpings
            sys.stdout.write(' '.ljust(skippedpings))
        for i in range(0,pingprobes):
            x=Process(target=one_packet_ping, args=(i*(60-realstartsec)/(pingprobes+1), IP, IP_family, pingresults, pingoptions))
            x.start()
        while printed < pingprobes:
            while pingresults.qsize()>0:
                pingresult=pingresults.get()
                if pingresult[0] == 0:
                    pingok=pingok+1
                    pingtime=pingresult[1]
                    sys.stdout.write('.')
                    if pingtime < l_min_time: l_min_time=pingtime
                    if pingtime > l_max_time: l_max_time=pingtime
                    l_all_time=l_all_time+pingtime
                else:
                    packet_loss=packet_loss+1
                    all_downtime=all_downtime+((60-startsec)/pingprobes)
                    if pingresult[0] == 1:
                        sys.stdout.write('x')
                    else: # pingresult[0] == 2
                        sys.stdout.write('?')
                printed=printed+1
                sys.stdout.flush()
            time.sleep(0.1)
        exit_ok = True
    except KeyboardInterrupt:
        exit_ok = False
    print_stat(pingprobes, printed, pingok, packet_loss)
    return (printed, packet_loss, min(min_time, l_min_time), max(max_time, l_max_time), all_time+l_all_time, all_downtime, exit_ok)

def human_readable_time(s):
    if s < 60:
        return f"{ s } sec"
    m = s // 60
    if m < 60:
        return f"{ m } min { human_readable_time(s-m*60) }" 
    h = m // 60
    if h < 24:
        return f"{ h } hour { human_readable_time(s-h*60*60) }"
    d = h // 24
    return f"{ d } day { human_readable_time(s-d*60*60*24) }"


def main():
    pingoptions = []

    ### Arguments
    parser = argparse.ArgumentParser()
    parser.add_argument('host', action='store')
    ppm_min = 10
    ppm_max = 30
    ppm_default = 30
    parser.add_argument('--ppm', help=f"packet per minute (default={ ppm_default }, min={ ppm_min }, max={ ppm_max })", default=ppm_default, type=int)
    parser.add_argument('-I', help='interface (original option of ping)', dest='interface')
    parser.add_argument('-s', help='use <size> as number of data bytes to be sent (original option of ping)', dest='size', type=int)
    parser.add_argument('-t', help='define time to live (original option of ping)', dest='ttl', type=int)
    parser.add_argument('-Q', help='use quality of service <tclass> bits (original option of ping)', dest='q', type=str)
    parser.add_argument('-b', help='allow pinging broadcast (original option of ping)', dest='broadcast', action='store_true')
    ipv4_or_ipv6 = parser.add_mutually_exclusive_group()
    ipv4_or_ipv6.add_argument('-4', help='force IPv4', action='store_true')
    ipv4_or_ipv6.add_argument('-6', help='force IPv6', action='store_true')
    args = parser.parse_args()

    address_family = None
    if vars(args)['4']:
        address_family = socket.AF_INET
    if vars(args)['6']:
        address_family = socket.AF_INET6
    if args.ppm >= ppm_min and args.ppm <= ppm_max:
        pingprobes = args.ppm
    else:
        raise ValueError(f"wrong ppm value (min={ ppm_min }, max={ ppm_max })")
    if args.interface:
        pingoptions=pingoptions + [ '-I', args.interface ]
    if args.size:
        pingoptions=pingoptions + [ '-s', str(args.size) ]
    if args.ttl:
        pingoptions=pingoptions + [ '-t', str(args.ttl) ]
    if args.broadcast:
        pingoptions=pingoptions + [ '-b' ]
    if args.q:
        pingoptions=pingoptions + [ '-Q', args.q ]
    ### /Arguments

    try:
        if address_family:
            addrinfo=socket.getaddrinfo(args.host, 0, family = address_family)[0]
        else:
            addrinfo=socket.getaddrinfo(args.host, 0)[0]
    except socket.gaierror as e:
        print(f'pinglong: {args.host}: {str(e)}', file=sys.stderr)
        sys.exit(1)

    try:

        IP_family=addrinfo[0]
        IP=addrinfo[4][0]

        if args.host == IP:
            hostinfo = args.host
        else:
            hostinfo = f"{ args.host } ({ IP })"
        print(f"PING { hostinfo }   { pingprobes } packet per minute")
        if not first_ping_test(IP, IP_family, pingoptions):
            sys.exit(1)

        lastheaderprinthour = 'X'
        pingn = 0
        packet_loss = 0
        min_time = 1000000.0
        max_time = 0.0
        all_time = 0
        all_downtime = 0
        starttime = time.time()
        first = True
        while True:
            current=time.strftime('%H%M')
            if current[:2] != lastheaderprinthour:
                print_header(pingprobes)
                lastheaderprinthour=current[:2]
            previous=current
            pingn, packet_loss, min_time, max_time, all_time, all_downtime, exit_ok = tuple(map(operator.add,
                    (pingn, packet_loss, 0, 0, 0, 0, False),
                    one_minute_pings(pingprobes, IP, IP_family, min_time, max_time, all_time, all_downtime, pingoptions, first)
                ))
            first = False
            if not exit_ok:
                raise KeyboardInterrupt
            while current == previous:
                current=time.strftime('%H%M')
                time.sleep(0.1)
    except KeyboardInterrupt:
        endtime = time.time()
        print('')
        if pingn > 0:
            print(f"PING { hostinfo } finished after { human_readable_time(int(endtime-starttime)) }.")
            print(f"Total: { pingn } packets transmitted, { int(pingn-packet_loss) } received, { round(100*packet_loss/pingn) }% packet loss")
            if pingn-packet_loss > 0:
                print(f"       RTT: min/avg/max = { min_time }/{ str(all_time/(pingn-packet_loss))[:6] }/{ max_time } ms")
            if all_downtime > 0:
                print(f"       Downtime: sum =~ { human_readable_time(round(all_downtime)) }")
        sys.exit(0)


if __name__ == "__main__":
    main()

