from typing import List
import requests as req
from pandas import DataFrame
import time as timelib

# TODO add typing

uploadDataset = "/api/deviceApi/uploadDataset"
initDatasetIncrement = "/api/deviceApi/initDatasetIncrement"
addDatasetIncrement = "/api/deviceApi/addDatasetIncrement"
addDatasetIncrementBatch = "/api/deviceApi/addDatasetIncrementBatch"
getProjectEndpoint = "/api/deviceApi/getProject"

#
#  Uploads a whole dataset to a specific project
#  @param {string} url - The url of the backend server
#  @param {string} key - The Device-Api-Key
#  @param {object} dataset - The dataset to upload
#  @returns A Promise indicating success or failure
#

def sendDataset(url: str, key: str, dataset: dict):
    try:
        res = req.post(url + uploadDataset, json = {"key": key, "payload": dataset})
    except req.exceptions.RequestException:
        raise "error" #TODO
    
#
# Returns the all datasets and labels belonging to a project
# Can be used for further processing
# @param {string} url - The url of the backend server
# @param {string} key - The Device-Api-Key
#

def getProject(url: str, key: str):
    res = req.post(url + getProjectEndpoint, json = {"key": key})
    return res.json()

def __extractLabels(dataset):
    labels = dataset['labels']
    labelType = "No labeling, no dataframe will be generated"
    if labels:
        labelType = labels[0][0]['labelingName']
    labelset = {} # stores different start and end times (intervals) belonging to a label
    labelIds = {} # assing distinct ids to labels, required for training with data
    labelId = 0
    for labelData in labels:
        for label in labelData:
            name = label['name']
            start = label['start']
            end = label['end']
            if not name in labelset:
                labelset[name] = []
                labelIds[name] = labelId        # assign id to the label
                labelId = labelId + 1               
            labelset[name].append((start, end)) # add interval to the label
    return (labelType, labelset, labelIds)        

def __processDataset(dataset):
    dataTimeValueSensor = {}                            # sensor values fused into single timestamps  
    sensors = dataset['sensors']
    for sensor in sensors:
        sensorName = sensor['name']
        data = sensor['data']
        for dataPoint in data:
            timestamp = dataPoint['timestamp']
            dataPointValue = dataPoint['datapoint']
            if timestamp not in dataTimeValueSensor:
                dataTimeValueSensor[timestamp] = []
            dataTimeValueSensor[timestamp].append({'value': dataPointValue, 'sensor': sensorName})
    return dataTimeValueSensor

#
# Returns a list of Pandas.DataFrames generated from the dataset
# @param {string} url - The url of the backend server
# @param {string} key - The Device-Api-Key
# 

def getDataFrames(url: str, key: str) -> List[DataFrame]:
    datasets = getProject(url, key)['datasets']
    dataFrames: List[DataFrame] = []
    for dataset in datasets:
        (labelType, labelset, labelIds) = __extractLabels(dataset)
        dataTimeValueSensor = __processDataset(dataset)
        dataFrame = {'id': [], labelType: []}
        id = 0
        for timestamp, timestampData in dataTimeValueSensor.items():
            for data in timestampData:
                value = data['value']
                sensor = data['sensor']
                if not sensor in dataFrame:
                    dataFrame[sensor] = []
                labelFound = False
                for label, intervals in labelset.items():
                    for interval in intervals:
                        start = interval[0]
                        end = interval[1]
                        if timestamp >= start and timestamp <= end:
                            if data == timestampData[0]:
                                dataFrame[labelType].append(label)
                                dataFrame['id'].append(id)
                                id = id + 1
                            dataFrame[sensor].append(value)
                            break
        dataFrame = DataFrame(dataFrame)
        dataFrames.append(dataFrame)
    return dataFrames

#
#  @param {string} url - The url of the backend server
#  @param {string} key - The Device-Api-Key
#  @param {boolean} useDeviceTime - True if you want to use timestamps generated by the server
#  @returns Function to upload single datapoints to one dataset inside a specific project
#
class datasetCollector():
    def __init__(self, url: str, key: str, name: str, useDeviceTime: bool) -> None:
        self.url = url
        self.key = key
        self.name = name
        self.useDeviceTime = useDeviceTime

        res = req.post(url + initDatasetIncrement, json = {"deviceApiKey": key, "name": name})
        #TODO error handling
        self.datasetKey = res.json()['datasetKey']
        self.dataStore = {'datasetKey': self.datasetKey, 'data': []}
        self.counter = 0
        self.error = None
    

    def addDataPoint(self, sensorName: str, value: float, time: int = None):
        if (self.error):
            raise self.error
        if (type(value) is not float): #TODO cast int to float, it may cause problems, can value be ever int?
            raise ValueError("Datapoint is not a number")
        if (not self.useDeviceTime and type(time) is not int and type(time) is not float):
            raise ValueError("Provide a valid timestamp")
        
        if (self.useDeviceTime):
            time = timelib.time()

        if (all(dataPoint['sensorname'] != sensorName for dataPoint in self.dataStore['data'])):
            self.dataStore['data'].append({
                'sensorname': sensorName, #TODO sensorname is not in camelcase, maybe refactor later in db?
                'start': time,
                'end': time,
                'timeSeriesData': [{'timestamp': time, 'datapoint': value}]
            })
        else:
            for dataPoint in self.dataStore['data']:
                if (dataPoint['sensorname'] == sensorName):
                    dataPoint['timeSeriesData'].append({'timestamp': time, 'datapoint': value})
                    dataPoint['start'] = min(dataPoint['start'], time)
                    dataPoint['end'] = max(dataPoint['end'], time)
                    break
        
        self.counter = self.counter + 1
        if self.counter > 1000:
            self.upload()

    def __upload(self):
        res = req.post(self.url + addDatasetIncrementBatch, json = self.dataStore)
        self.counter = 0
        self.dataStore = {'datasetKey': self.datasetKey, 'data': []}

    def onComplete(self):
        if self.error:
            raise self.error
        self.__upload()

getDataFrames("https://app.edge-ml.org", "KVH0X0i9GIM/aP6IUPcr88iSaCRhh8uTWsnyrz5651X7fbvPFXrU/qoFCEgCVEv/xMZA4QDaNWpaeVypImfMsw==")