# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
import os
import io
import json

import pandas as pd
import zipfile as zf

def nsToMs(ns):
    return ns / 1000000

def appendEvent(data, event):
    data['processId'].append(event['pid'])
    data['threadId'].append(event['tid'])
    data['type'].append(event['ph'])
    data['category'].append(event['cat'])
    data['name'].append(event['name'])
    data['begin'].append(nsToMs(event['ts']))

    duration = 1.0
    command = ""
    counter = int(-1)

    if event['cat'] == "Initialize":
        command = "Initialize"

    if "dur" in event['args']:
        duration = nsToMs(event['args']['dur'])

    if "name" in event['args']:
        command = event["args"]["name"].split("::")[-1]

    if "counter" in event['args']:
        counter = event['args']['counter']

    data['duration'].append(duration)
    data['command'].append(command)
    data['counter'].append(counter)

def readTraceFile(file):
    pdata = {
        "processId": [],
        "processName": []
    }
    tdata = {
        "threadId": [],
        "threadName": []
    }
    data = {
        "processId": [],
        "threadId": [],
        "type": [],
        "category": [],
        "name": [],
        "begin": [],
        "duration": [],
        "command": [],
        "counter": []
    }

    for event in json.loads(file)["traceEvents"]:
        if event["ph"] == "M" and event["name"] == "process_name":
            pdata["processId"].append(event["pid"])
            pdata["processName"].append(event["args"]["name"])
        elif event["ph"] == "M" and event["name"] == "thread_name":
            tdata["threadId"].append(event["tid"])
            tdata["threadName"].append(event["args"]["name"])
        else:
            appendEvent(data, event)

    pdf = pd.DataFrame(pdata)
    tdf = pd.DataFrame(tdata)
    ddf = pd.DataFrame(data)

    # Replace pid/tid with processName/threadName and sort.
    df = ddf.merge(right=pdf).merge(right=tdf)
    df.drop(inplace=True, columns=["processId", "threadId"])
    df.sort_values("begin").reset_index(inplace=True, drop=True)

    # Merge Begin/End rows
    for idx, row in df.iterrows():
        if row['type'] == 'B':
            nextEndMask = (df['type']=='E') & (df.index>idx)
            relatedMask = (df['category']==row['category']) & (df['name']==row['name'])
            nextRelatedRow = df.loc[nextEndMask & relatedMask].head(1).squeeze()

            if nextRelatedRow.empty:
                df.loc[idx, 'duration'] = 1.0
            else:
                df.loc[idx, 'type'] = 'BE'
                df.loc[idx, 'duration'] = nextRelatedRow['begin'] - row['begin']

    # Make columns categorial
    categories = ['type', 'category', 'name', 'command', 'processName', 'threadName']
    df[categories] = df[categories].astype('category')

    df.drop(df[ df['type']=='E'].index, inplace=True)
    df.reset_index(inplace=True, drop=True)

    return df

def computePuppetOffset(dfd, dfp):
    puppetSyncWriteMask = (dfp['category'] == "Sync") & (dfp['name']=="writeCommand") & (dfp['type']=="I")
    puppetSyncWriteDf = dfp.loc[ puppetSyncWriteMask ].head(1)
    puppetSyncWriteRow = puppetSyncWriteDf.squeeze()
    if puppetSyncWriteDf.empty: return 0.0

    cmdName = puppetSyncWriteRow['command']

    puppetSyncReadMask = (dfp['category'] == "Sync") & (dfp['name']=="readCommand") & (dfp['command']==cmdName)
    puppetSyncReadDf = dfp.loc[ puppetSyncReadMask ].head(1)
    puppetSyncReadRow = puppetSyncReadDf.squeeze()
    if puppetSyncReadDf.empty: return 0.0

    designerSyncReadMask = (dfd['category'] == "Sync") & (dfd['name']=="readCommand") & (dfd['command']==cmdName)
    designerSyncWriteDf = dfd.loc[ designerSyncReadMask ].head(1)
    designerSyncWriteRow = designerSyncWriteDf.squeeze()

    if designerSyncWriteDf.empty: return 0.0

    puppetCenter = puppetSyncReadRow['begin'] - puppetSyncWriteRow['begin']
    return designerSyncWriteRow['begin'] - puppetCenter

def SynchronizeDataframes(dfs):
    dfd = dfs['QmlDesigner']
    dfe = dfs['Puppet']['Editor']
    dfp = dfs['Puppet']['Preview']
    dfr = dfs['Puppet']['Render']
    dfe['begin'] = dfe['begin'] + computePuppetOffset(dfd, dfe)
    dfp['begin'] = dfp['begin'] + computePuppetOffset(dfd, dfp)
    dfr['begin'] = dfr['begin'] + computePuppetOffset(dfd, dfr)
    return dfs

def collectDataframes(dfs):
    out = { "QmlDesigner": {}, "Puppet": { "Editor": {}, "Preview": {}, "Render": {} } }
    for df in dfs:
        if not df.empty:
            df = df.sort_values(by="threadName", ascending=True)
            if df['processName'][0] == 'QmlDesigner':
                out['QmlDesigner'] = df.sort_index()
            elif df['processName'][0] == 'EditorModePuppet':
                out['Puppet']['Editor'] = df.sort_index()
            elif df['processName'][0] == 'PreviewModePuppet':
                out['Puppet']['Preview'] = df.sort_index()
            elif df['processName'][0] == 'RenderModePuppet':
                out['Puppet']['Render'] = df.sort_index()

    return SynchronizeDataframes(out)

def hasExtension(filename, ext):
    name, e = os.path.splitext(filename)
    return True if e == ext else False

def extractZipArchive(zip_file):
    dfs = []
    for filename in zip_file.namelist():
        invalid = filename.startswith('__MACOSX')
        if not invalid and hasExtension(filename, ".json"):
            with zip_file.open(filename) as file_object:
                data = file_object.read().strip()
                dfs.append(readTraceFile(data))
    return collectDataframes(dfs)

def openZipArchive(data):
    zip_str = io.BytesIO(data)
    try:
        with zf.ZipFile(zip_str, 'r') as zip_file:
            return extractZipArchive(zip_file)
    except IOError:
        print("Failed to open zip file: \"" + path + "\"")
        sys.exit(2)

def readZipArchive(path):
    try:
        with zf.ZipFile(path) as zip_file:
            return extractZipArchive(zip_file)
    except IOError:
        print("Failed to read zip file: \"" + path + "\"")
        sys.exit(2)

def openTraceFile(path):
    try:
        with open(path, "r") as file_object:
            return readTraceFile(file_object.read())
    except IOError:
        print("Failed to open file: \"" + path + "\"")
        sys.exit(2)
