import LineProtocolGenerator as lp
import time
import urllib.parse
import urllib.request
import sys
import random

# Simulation parameters
TICK_TIME = 1
DEFAULT_REQUEST_RATE_INC = 1
DEFAULT_REQUEST_RATE_INC_PERIOD = 10 
SIMULATION_TIME_SEC = 60*60

# CLMC parameters
INFLUX_DB_URL = 'http://192.168.50.10:8086'
AGENT_URL1 = 'http://192.168.50.11:8186'
AGENT_URL2 = 'http://192.168.50.12:8186'

# Simulator for services
class sim:
    def __init__(self, influx_url):
        # We don't need this as the db is CLMC metrics
        self.influx_db = 'CLMCMetrics'
        self.influx_url = influx_url
        # Teardown DB from previous sim and bring it back up
        self._deleteDB()
        self._createDB()


    def run(self, simulation_length_seconds):
        start_time = time.time()-SIMULATION_TIME_SEC
        sim_time = start_time

        # segment_size : the length of video requested at a time
        # bit_rate: MPEG-2 High 1080p 25fps = 80Mbps
        ip_endpoints = [{'agent_url': AGENT_URL1, 'location': 'DC1', 'cpu': 16,
                        'mem': '8GB', 'storage': '1TB', 'request_queue': 0, 'request_arrival_rate': 0,
                        'segment_size': 2, 'video_bit_rate': 80, 'packet_size': 1500},
                        {'agent_url': AGENT_URL2, 'location': 'DC2', 'cpu': 4, 
                        'mem': '8GB', 'storage': '1TB', 'request_queue': 0, 'request_arrival_rate': 0, 
                        'segment_size': 2, 'video_bit_rate': 80, 'packet_size': 1500}
                        ]

        # Simulate configuration of the ipendpoints
        # endpoint state->mu, sigma, secs normal distribution
        config_delay_dist = {"placing": [10, 0.68], "booting": [10, 0.68],"connecting": [10, 0.68]}

        # Place endpoints
        max_delay = 0              
        for ip_endpoint in ip_endpoints:
            delay_time = self._changeVMState(sim_time, ip_endpoint, config_delay_dist['placing'][0], config_delay_dist['placing'][0]*config_delay_dist['placing'][1], 'placing', 'placed')
            if delay_time > max_delay:
                max_delay = delay_time
        sim_time +=max_delay

        # Boot endpoints
        max_delay = 0        
        for ip_endpoint in ip_endpoints:
            delay_time = self._changeVMState(sim_time, ip_endpoint, config_delay_dist['booting'][0], config_delay_dist['booting'][0]*config_delay_dist['booting'][1], 'booting', 'booted')
            if delay_time > max_delay:
                max_delay = delay_time            
        sim_time +=max_delay

        # Connect endpoints
        max_delay = 0     
        for ip_endpoint in ip_endpoints:
            delay_time = self._changeVMState(sim_time, ip_endpoint, config_delay_dist['connecting'][0], config_delay_dist['connecting'][0]*config_delay_dist['connecting'][1], 'connecting', 'connected')
            if delay_time > max_delay:
                max_delay = delay_time
        sim_time +=max_delay
   
        request_arrival_rate_inc = DEFAULT_REQUEST_RATE_INC
        request_queue = 0
        inc_period_count = 0
        for i in range(simulation_length_seconds):        
            for ip_endpoint in ip_endpoints:
                request_processing_time = 0
                cpu_time_available = 0
                requests_processed = 0
                max_requests_processed = 0
                cpu_active_time = 0
                cpu_idle_time = 0
                cpu_usage = 0
                cpu_load_time = 0
                avg_response_time = 0
                peak_response_time = 0

                # linear inc to arrival rate
                if inc_period_count >= DEFAULT_REQUEST_RATE_INC_PERIOD:
                    ip_endpoint['request_arrival_rate'] += request_arrival_rate_inc
                    inc_period_count = 0
                else:
                    inc_period_count += 1
                # add new requests to the queue
                ip_endpoint['request_queue'] += ip_endpoint['request_arrival_rate']

                # time to process one second of video (mS) in the current second
                request_processing_time = int(random.normalvariate(10, 10*0.68))
                if request_processing_time <= 10:
                    request_processing_time = 10
                # time depends on the length of the segments in seconds
                request_processing_time *= ip_endpoint['segment_size']

                # amount of cpu time (mS) per tick
                cpu_time_available = ip_endpoint['cpu']*TICK_TIME*1000
                max_requests_processed = int(cpu_time_available/request_processing_time)
                # calc how many requests processed
                if ip_endpoint['request_queue'] <= max_requests_processed:
                    # processed all of the requests
                    requests_processed = ip_endpoint['request_queue']
                else:
                    # processed the maxmum number of requests
                    requests_processed = max_requests_processed

                # calculate cpu usage
                cpu_active_time = int(requests_processed*request_processing_time)
                cpu_idle_time = int(cpu_time_available-cpu_active_time)
                cpu_usage = cpu_active_time/cpu_time_available
                self._sendInfluxData(ip_endpoint['agent_url'], lp.generate_cpu_report(cpu_usage, cpu_active_time, cpu_idle_time, sim_time))

                # calc network usage metrics
                bytes_rx = 2048*requests_processed           
                bytes_tx = int(ip_endpoint['video_bit_rate']/8*1000000*requests_processed*ip_endpoint['segment_size'])
                self._sendInfluxData(ip_endpoint['agent_url'], lp.generate_network_report(bytes_rx, bytes_tx, sim_time))                

                # time to process all of the requests in the queue
                peak_response_time = ip_endpoint['request_queue']*request_processing_time/ip_endpoint['cpu']
                # mid-range 
                avg_response_time = (peak_response_time+request_processing_time)/2
                self._sendInfluxData(ip_endpoint['agent_url'], lp.generate_mpegdash_report('http://localhost/server-status?auto', ip_endpoint['request_arrival_rate'], avg_response_time, peak_response_time, sim_time))

                # need to calculate this but sent at 5mS for now
                network_request_delay = 0.005

                # calculate network response delays (2km link, 100Mbps)
                network_response_delay = self._calcNetworkDelay(2000, 100, ip_endpoint['packet_size'], ip_endpoint['video_bit_rate'], ip_endpoint['segment_size'])

                e2e_delay = network_request_delay + (avg_response_time/1000) + network_response_delay

                self._sendInfluxData(ip_endpoint['agent_url'], lp.generate_ipendpoint_route('http://localhost/server-status?auto', ip_endpoint['request_arrival_rate'], e2e_delay, sim_time))

                # remove requests processed off the queue
                ip_endpoint['request_queue'] -= int(requests_processed)            

            sim_time += TICK_TIME
        end_time = sim_time
        print("Simulation Finished. Start time {0}. End time {1}. Total time {2}".format(start_time,end_time,end_time-start_time))

    # distance metres
    # bandwidth Mbps
    # package size bytes
    # tx_video_bit_rate bp/sec
    # segment size sec
    def _calcNetworkDelay(self, distance, bandwidth, packet_size, tx_video_bit_rate, segment_size):
        response_delay = 0

        # propogation delay = distance/speed () (e.g 2000 metres * 2*10^8 for optical fibre)
        propogation_delay = distance/(2*100000000)
        # packetisation delay = ip packet size (bits)/tx rate (e.g. 100Mbp with  0% packet loss)
        packetisation_delay = (packet_size*8)/(bandwidth*1000000)
    #    print('packetisation_delay:', packetisation_delay)   
        # total number of packets to be sent
        packets = (tx_video_bit_rate*1000000)/(packet_size*8)
     #   print('packets:', packets)        
        response_delay = packets*(propogation_delay+packetisation_delay)
      #  print('response_delay:', response_delay)

        return response_delay     

    def _changeVMState(self, sim_time, ip_endpoint, mu, sigma, transition_state, next_state):
        delay_time = 0
    
        self._sendInfluxData(ip_endpoint['agent_url'], lp.generate_vm_config(transition_state, ip_endpoint['cpu'], ip_endpoint['mem'], ip_endpoint['storage'], sim_time))   
        
        delay_time = random.normalvariate(mu, sigma)        
        
        self._sendInfluxData(ip_endpoint['agent_url'], lp.generate_vm_config(next_state, ip_endpoint['cpu'], ip_endpoint['mem'], ip_endpoint['storage'], sim_time+delay_time))

        return delay_time

    def _createDB(self):
        self._sendInfluxQuery(self.influx_url, 'CREATE DATABASE ' + self.influx_db)


    def _deleteDB(self):
        self._sendInfluxQuery(self.influx_url, 'DROP DATABASE ' + self.influx_db)


    def _sendInfluxQuery(self, url, query):
        query = urllib.parse.urlencode({'q': query})
        query = query.encode('ascii')
        req = urllib.request.Request(url + '/query ', query)
        urllib.request.urlopen(req)

    def _sendInfluxData(self, url, data):
        data = data.encode()
        header = {'Content-Type': 'application/octet-stream'}
        req = urllib.request.Request(url + '/write?db=' + self.influx_db, data, header)
        urllib.request.urlopen(req)  

simulator = sim(INFLUX_DB_URL)
simulator.run(SIMULATION_TIME_SEC)