function uint8ArrayToBase64(uint8array) {
    return btoa(String.fromCharCode.apply(null, uint8array));
}

function uint8ArrayToString(uint8array) {
    return String.fromCharCode.apply(null, uint8array)
}

function base64toUint8Array(base64_string){
    return Uint8Array.from(atob(base64_string), c => c.charCodeAt(0))
}

function compareUint8Arrays(arr1, arr2) {
    if (arr1.length !== arr2.length)
        return false;
    return Array.from(arr1).every((value, index) => value === arr2[index]);
}

function emitAndWaitForResponse(type, data) {
    return new Promise((resolve) => {
        const requestId = Math.random().toString(16).substring(2, 9);
        const responseHandler = (event) => {
            const { detail } = event;
            if (detail.substring(0, 7) === requestId) {
                document.removeEventListener('responseReceived', responseHandler);
                resolve(detail.substring(7));
            }
        };
        document.addEventListener('responseReceived', responseHandler);
        const requestEvent = new CustomEvent('response', {
            detail: {
                type: type,
                body: data,
                requestId: requestId,
            }
        });
        document.dispatchEvent(requestEvent);
    });
}

const fnproxy = (object, func) => new Proxy(object, { apply: func });
const proxy = (object, key, func) => Object.hasOwnProperty.call(object, key) && Object.defineProperty(object, key, {
    value: fnproxy(object[key], func)
});

function getEventListeners(type) {
    if (this == null) return [];
    const store = this[Symbol.for(getEventListeners)];
    if (store == null || store[type] == null) return [];
    return store[type];
}

class Evaluator {
    static isDASH(text) {
        return text.includes('<mpd') && text.includes('</mpd>');
    }

    static isHLS(text) {
        return text.includes('#extm3u');
    }

    static isHLSMaster(text) {
        return text.includes('#ext-x-stream-inf');
    }

    static isMSS(text) {
        return text.includes('<smoothstreamingmedia') && text.includes('</smoothstreamingmedia>');
    }

    static getManifestType(text) {
        const lower = text.toLowerCase();
        if (this.isDASH(lower)) {
            return "DASH";
        } else if (this.isHLS(lower)) {
            if (this.isHLSMaster(lower)) {
                return "HLS_MASTER";
            } else {
                return "HLS_PLAYLIST";
            }
        } else if (this.isMSS(lower)) {
            return "MSS";
        }
    }
}

(async () => {
    if (typeof EventTarget !== 'undefined') {
        proxy(EventTarget.prototype, 'addEventListener', async (_target, _this, _args) => {
            if (_this != null) {
                const [type, listener] = _args;

                const storeKey = Symbol.for(getEventListeners);
                if (!(storeKey in _this)) _this[storeKey] = {};

                const store = _this[storeKey];
                if (!(type in store)) store[type] = [];
                const listeners = store[type];

                let wrappedListener = listener;
                if (type === "message" && !!listener && !listener._isWrapped) {
                    wrappedListener = async function(event) {
                        if (event instanceof MediaKeyMessageEvent) {
                            if (event._isCustomEvent) {
                                if (listener.handleEvent) {
                                    listener.handleEvent(event);
                                } else {
                                    listener.call(this, event);
                                }
                                return;
                            }

                            let newBody = new Uint8Array(event.message);
                            if (!compareUint8Arrays(new Uint8Array([0x08, 0x04]), new Uint8Array(event.message))) {
                                console.log("[WidevineProxy2]", "WIDEVINE_PROXY", "MESSAGE", listener);
                                if (listener.name !== "messageHandler") {
                                    const oldChallenge = uint8ArrayToBase64(new Uint8Array(event.message));
                                    const newChallenge = await emitAndWaitForResponse("REQUEST", oldChallenge);
                                    if (oldChallenge !== newChallenge) {
                                        // Playback will fail if the challenges are the same (aka. the background script
                                        // returned the same challenge because the addon is disabled), but I still
                                        // override the challenge anyway, so check beforehand (in base64 form)
                                        newBody = base64toUint8Array(newChallenge);
                                    }
                                } else {
                                    // trick EME Logger
                                    // better suggestions for avoiding EME Logger interference are welcome
                                    await emitAndWaitForResponse("REQUEST", "");
                                }
                            }

                            const newEvent = new MediaKeyMessageEvent('message', {
                                isTrusted: event.isTrusted,
                                bubbles: event.bubbles,
                                cancelBubble: event.cancelBubble,
                                composed: event.composed,
                                currentTarget: event.currentTarget,
                                defaultPrevented: event.defaultPrevented,
                                eventPhase: event.eventPhase,
                                message: newBody.buffer,
                                messageType: event.messageType,
                                returnValue: event.returnValue,
                                srcElement: event.srcElement,
                                target: event.target,
                                timeStamp: event.timeStamp,
                            });
                            newEvent._isCustomEvent = true;

                            _this.dispatchEvent(newEvent);
                            event.stopImmediatePropagation();
                            return
                        }

                        if (listener.handleEvent) {
                            listener.handleEvent(event);
                        } else {
                            listener.call(this, event);
                        }
                    };

                    wrappedListener._isWrapped = true;
                    wrappedListener.originalListener = listener;
                }

                const alreadyAdded = listeners.some(
                    storedListener => storedListener && storedListener.originalListener === listener
                );

                if (!alreadyAdded) {
                    listeners.push(wrappedListener);
                    _args[1] = wrappedListener;
                }
            }
            return _target.apply(_this, _args);
        });
    }

    if (typeof MediaKeySession !== 'undefined') {
        proxy(MediaKeySession.prototype, 'update', async (_target, _this, _args) => {
            const [response] = _args;
            console.log("[WidevineProxy2]", "WIDEVINE_PROXY", "UPDATE");
            await emitAndWaitForResponse("RESPONSE", uint8ArrayToBase64(new Uint8Array(response)))
            return await _target.apply(_this, _args);
        });
    }
})();

const originalFetch = window.fetch;
window.fetch = function() {
    return new Promise(async (resolve, reject) => {
        originalFetch.apply(this, arguments).then((response) => {
            if (response) {
                response.clone().text().then((text) => {
                    const manifest_type = Evaluator.getManifestType(text);
                    if (manifest_type) {
                        if (arguments.length === 1) {
                            emitAndWaitForResponse("MANIFEST", JSON.stringify({
                                "url": arguments[0].url,
                                "type": manifest_type,
                            }));
                        } else if (arguments.length === 2) {
                            emitAndWaitForResponse("MANIFEST", JSON.stringify({
                                "url": arguments[0],
                                "type": manifest_type,
                            }));
                        }
                    }
                    resolve(response);
                }).catch(() => {
                    resolve(response);
                })
            } else {
                resolve(response);
            }
        }).catch(() => {
            resolve();
        })
    })
}

const open = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function(method, url) {
    this._method = method;
    return open.apply(this, arguments);
};

const send = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.send = function(postData) {
    this.addEventListener('load', async function() {
        if (this._method === "GET") {
            let body = void 0;
            switch (this.responseType) {
                case "":
                case "text":
                    body = this.responseText ?? this.response;
                    break;
                case "json":
                    // TODO: untested
                    body = JSON.stringify(this.response);
                    break;
                case "arraybuffer":
                    // TODO: untested
                    if (this.response.byteLength) {
                        const response = new Uint8Array(this.response);
                        body = uint8ArrayToString(new Uint8Array([...response.slice(0, 2000), ...response.slice(-2000)]));
                    }
                    break;
                case "document":
                    // todo
                    break;
                case "blob":
                    body = await this.response.text();
                    break;
            }
            if (body) {
                const manifest_type = Evaluator.getManifestType(body);
                if (manifest_type) {
                    emitAndWaitForResponse("MANIFEST", JSON.stringify({
                        "url": this.responseURL,
                        "type": manifest_type,
                    }));
                }
            }
        }
    });
    return send.apply(this, arguments);
};
