#!/usr/bin/env python3

import random # For simulation, not cryptography!
import math
import sys
import os
import logging
import resource
import sympy
import re
import statistics

sys.path.append("..")

import network
import dirauth
import relay
import client

from bwparser import JansenBandwidthParser, KomloBandwidthParser

class BandwidthMeasurer:
    def __init__(self, total_numrelays, bw_parser, numdirauths, numrelays, numclients):
        self.total_size = total_numrelays
        self.bw_parser = bw_parser

        # Start some dirauths
        self.dirauthaddrs = []
        self.dirauths = []
        for i in range(numdirauths):
            dira = dirauth.DirAuth(i, numdirauths)
            self.dirauths.append(dira)
            self.dirauthaddrs.append(dira.netaddr)

        # Start some relays
        self.relays = []
        for i in range(numrelays):
            self.startrelay()

        # The fallback relays are a hardcoded list of a small fraction
        # of the relays, used by clients for bootstrapping
        numfallbackrelays = 1
        fallbackrelays = self.relays[0:1]
        for r in fallbackrelays:
            r.set_is_fallbackrelay()
        network.thenetwork.setfallbackrelays(fallbackrelays)

        # Tick the epoch to build the first consensus
        network.thenetwork.nextepoch()

        # Start some clients
        self.clients = []
        for i in range(numclients):
            self.startclient()

        # Throw away all the performance statistics to this point
        for d in self.dirauths: d.perfstats.reset()
        for r in self.relays: r.perfstats.reset()
        # The clients' stats are already at 0, but they have the
        # "bootstrapping" flag set, which we want to keep, so we
        # won't reset them.

        self.allcircs = []

        # Tick the epoch to bootstrap the clients
        network.thenetwork.nextepoch()

    def startrelay(self):
        bw = int(self.calculate_relay_bandwidth())
        new_relay = relay.Relay(self.dirauthaddrs, bw, 0)
        self.relays.append(new_relay)

    def calculate_relay_bandwidth(self):
        return random.choice(bw_parser.get_distribution())

    def stoprelay(self):
        self.relays[1].terminate()
        del self.relays[1]

    def startclient(self):
        self.clients.append(client.Client(self.dirauthaddrs))

    def stopclient(self):
        self.clients[0].terminate()
        del self.clients[0]

    def buildcircuit(self):
        bwm.allcircs.append(bwm.clients[0].channelmgr.new_circuit())

    def getstats(self):

        # gather stats
        totsent = 0
        totrecv = 0
        totbytes = 0
        dirasent = 0
        dirarecv = 0
        dirabytes = 0
        relaysent = 0
        relayrecv = 0
        relaybytes = 0
        clisent = 0
        clirecv = 0
        clibytes = 0
        for d in self.dirauths:
            logging.debug("%s", d.perfstats)
            dirasent += d.perfstats.bytes_sent
            dirarecv += d.perfstats.bytes_received
            dirabytes += d.perfstats.bytes_sent + d.perfstats.bytes_received
        totsent += dirasent
        totrecv += dirarecv
        totbytes += dirabytes
        for r in self.relays:
            logging.debug("%s", r.perfstats)
            relaysent += r.perfstats.bytes_sent
            relayrecv += r.perfstats.bytes_received
            relaybytes += r.perfstats.bytes_sent + r.perfstats.bytes_received
        totsent += relaysent
        totrecv += relayrecv
        totbytes += relaybytes
        for c in self.clients:
            logging.debug("%s", c.perfstats)
            clisent += c.perfstats.bytes_sent
            clirecv += c.perfstats.bytes_received
            clibytes += c.perfstats.bytes_sent + c.perfstats.bytes_received
        totsent += clisent
        totrecv += clirecv
        totbytes += clibytes
        logging.info("DirAuths sent=%s recv=%s bytes=%s" % \
                (dirasent, dirarecv, dirabytes))
        logging.info("Relays sent=%s recv=%s bytes=%s" % \
                (relaysent, relayrecv, relaybytes))
        logging.info("Client sent=%s recv=%s bytes=%s" % \
                (clisent, clirecv, clibytes))
        logging.info("Total sent=%s recv=%s bytes=%s" % \
                (totsent, totrecv, totbytes))

        # Reset bootstrap flag
        for d in self.dirauths: d.perfstats.is_bootstrapping = False
        for r in self.relays: r.perfstats.is_bootstrapping = False
        for c in self.clients: c.perfstats.is_bootstrapping = False

        return (dirabytes, relaybytes, clibytes)

    def endepoch(self):

        # Close circuits
        for c in self.allcircs:
            c.close()
        self.allcircs = []

        # Reset stats
        for d in self.dirauths: d.perfstats.reset()
        for r in self.relays: r.perfstats.reset()
        for c in self.clients: c.perfstats.reset()

        network.thenetwork.nextepoch()


if __name__ == '__main__':
    # Args: womode snipauthmode numrelays randseed
    if len(sys.argv) != 5:
        sys.stderr.write("Usage: womode snipauthmode numrelays randseed\n")
        sys.exit(1)

    bandwidth_file = os.getenv('BW_FILE')

    womode = network.WOMode[sys.argv[1].upper()]
    snipauthmode = network.SNIPAuthMode[sys.argv[2].upper()]
    numrelays = int(sys.argv[3])
    randseed = int(sys.argv[4])

    bw_parser = None
    if os.getenv('BW_ALGO') != "komlo":
        bw_parser = JansenBandwidthParser(bw_file=bandwidth_file, sample_size=numrelays)
    else:
        # keep the original assumption
        bw_parser = KomloBandwidthParser(sample_size=numrelays)

    total_size = bw_parser.get_relay_num()

    # Use symbolic byte counter mode
    network.symbolic_byte_counters = True

    # Seed the PRNG.  On Ubuntu 18.04, this in fact makes future calls
    # to (non-cryptographic) random numbers deterministic.  On Ubuntu
    # 16.04, it does not.
    random.seed(randseed)

    loglevel = logging.INFO
    # Uncomment to see all the debug messages
    # loglevel = logging.DEBUG

    logging.basicConfig(level=loglevel,
            format="%(asctime)s:%(levelname)s:%(message)s")

    logging.info("Starting simulation")

    # Set the Walking Onions style to use
    network.thenetwork.set_wo_style(womode, snipauthmode)

    bwm = BandwidthMeasurer(total_size, bw_parser, 9, numrelays, 0)
    stats = dict()

    logging.info("R_N = %d, R_B = 0, C_N = 0, C_B = 0, circs = 0", numrelays)
    stats[(numrelays, 0, 0, 0, 0)] = bwm.getstats()
    # Bootstrap one relay
    bwm.startrelay()
    bwm.endepoch()
    logging.info("R_N = %d, R_B = 1, C_N = 0, C_B = 0, circs = 0", numrelays)
    stats[(numrelays, 1, 0, 0, 0)] = bwm.getstats()
    # Bootstrap one client
    bwm.stoprelay()
    bwm.startclient()
    bwm.endepoch()
    logging.info("R_N = %d, R_B = 0, C_N = 0, C_B = 1, circs = 0", numrelays)
    stats[(numrelays, 0, 0, 1, 0)] = bwm.getstats()
    # No changes, so the client is now not bootstrapping
    bwm.endepoch()
    logging.info("R_N = %d, R_B = 0, C_N = 1, C_B = 0, circs = 0", numrelays)
    stats[(numrelays, 0, 1, 0, 0)] = bwm.getstats()
    # No more bootstrapping, but build one circuit
    bwm.buildcircuit()
    logging.info("R_N = %d, R_B = 0, C_N = 1, C_B = 0, circs = 1", numrelays)
    stats[(numrelays, 0, 1, 0, 1)] = bwm.getstats()
    bwm.endepoch()
    # No more bootstrapping, but build two circuits
    bwm.buildcircuit()
    bwm.buildcircuit()
    logging.info("R_N = %d, R_B = 0, C_N = 1, C_B = 0, circs = 2", numrelays)
    stats[(numrelays, 0, 1, 0, 2)] = bwm.getstats()
    bwm.endepoch()

    print("\n")
    print('Total relay bytes:')
    print('  R_N * (', stats[(numrelays, 0, 0, 0, 0)][1]/numrelays, ')')
    print('+ R_B * (', stats[(numrelays, 1, 0, 0, 0)][1] - stats[(numrelays, 0, 0, 0, 0)][1], ')')
    print('+ C_N * (', stats[(numrelays, 0, 1, 0, 0)][1] - stats[(numrelays, 0, 0, 0, 0)][1], ')')
    print('+ C_B * (', stats[(numrelays, 0, 0, 1, 0)][1] - stats[(numrelays, 0, 0, 0, 0)][1], ')')
    print('+ circ * (', stats[(numrelays, 0, 1, 0, 1)][1] - stats[(numrelays, 0, 1, 0, 0)][1], ')')
    print('  check   ', stats[(numrelays, 0, 1, 0, 2)][1] - stats[(numrelays, 0, 1, 0, 1)][1])

    print("\n")
    print('Total client bytes:')
    print('  R_N * (', stats[(numrelays, 0, 0, 0, 0)][2]/numrelays, ')')
    print('+ R_B * (', stats[(numrelays, 1, 0, 0, 0)][2] - stats[(numrelays, 0, 0, 0, 0)][2], ')')
    print('+ C_N * (', stats[(numrelays, 0, 1, 0, 0)][2] - stats[(numrelays, 0, 0, 0, 0)][2], ')')
    print('+ C_B * (', stats[(numrelays, 0, 0, 1, 0)][2] - stats[(numrelays, 0, 0, 0, 0)][2], ')')
    print('+ circ * (', stats[(numrelays, 0, 1, 0, 1)][2] - stats[(numrelays, 0, 1, 0, 0)][2], ')')
    print('  check   ', stats[(numrelays, 0, 1, 0, 2)][2] - stats[(numrelays, 0, 1, 0, 1)][2])
