declare let process: any;

namespace pxt {
    type Map<T> = { [index: string]: T };

    const eventBufferSizeLimit = 20;
    const queues: TelemetryQueue<any, any, any>[] = [];

    let analyticsLoaded = false;
    let interactiveConsent = false;
    let isProduction = false;
    let partnerName: string;

    class TelemetryQueue<A, B, C> {
        private q: [A, B, C][] = [];
        constructor(private log: (a?: A, b?: B, c?: C) => void) {
            queues.push(this);
        }

        public track(a: A, b: B, c: C) {
            if (analyticsLoaded) {
                this.log(a, b, c);
            }
            else {
                this.q.push([a, b, c]);
                if (this.q.length > eventBufferSizeLimit) this.q.shift();
            }
        }

        public flush() {
            while (this.q.length) {
                const [a, b, c] = this.q.shift();
                this.log(a, b, c);
            }
        }
    }

    let eventLogger: TelemetryQueue<string, Map<string>, Map<number>>;
    let exceptionLogger: TelemetryQueue<any, string, Map<string>>;

    type EventListener<T = any> = (ev: T) => void;
    type EventSource<T> = {
        subscribe(listener: (ev: T) => void): () => void;
        emit(ev: T): void;
        forEach(callback: (ev: T) => void): void;
    };

    function createEventSource<T = any>(filter?: ((ev: T) => boolean)): EventSource<T> {
        filter = filter || (() => true);
        const listeners: EventListener<T>[] = [];
        const eventCache: T[] = [];

        return {
            subscribe(listener: EventListener<T>): () => void {
                eventCache.forEach(ev => filter(ev) && listener(ev));
                listeners.push(listener);
                // Return an unsubscribe function
                return () => {
                    const index = listeners.indexOf(listener);
                    if (index !== -1) {
                        listeners.splice(index, 1);
                    }
                };
            },
            emit(ev: T): void {
                eventCache.push(ev);
                if (filter(ev)) listeners.forEach(listener => listener(ev));
            },
            forEach(callback: (ev: T) => void): void {
                eventCache.forEach(ev => filter(ev) && callback(ev));
            }
        };
    }

    // performance measuring, added here because this is amongst the first (typescript) code ever executed
    export namespace perf {
        let enabled: boolean;

        export let startTimeMs: number;
        export let measurementThresholdMs = 10;
        export let stats: {
            durations: EventSource<{ name: string, start: number, duration: number, params?: Map<string> }>,
            milestones: EventSource<{ milestone: string, time: number, params?: Map<string> }>,
        } = {
            durations: createEventSource((ev) => ev.duration >= measurementThresholdMs),
            milestones: createEventSource(),
        }
        export function isEnabled() { return enabled; }
        export let perfReportLogged = false
        export function splitMs(): number {
            return Math.round(performance.now() - startTimeMs)
        }
        export function prettyStr(ms: number): string {
            ms = Math.round(ms)
            let r_ms = ms % 1000
            let s = Math.floor(ms / 1000)
            let r_s = s % 60
            let m = Math.floor(s / 60)
            if (m > 0)
                return `${m}m${r_s}s`
            else if (s > 5)
                return `${s}s`
            else if (s > 0)
                return `${s}s${r_ms}ms`
            else
                return `${ms}ms`
        }
        export function splitStr(): string {
            return prettyStr(splitMs())
        }

        export function recordMilestone(msg: string, params?: Map<string>) {
            const time = splitMs()
            stats.milestones.emit({ milestone: msg, time, params });
        }
        export function init() {
            enabled = performance && !!performance.mark && !!performance.measure;
            if (enabled) {
                performance.measure("measure from the start of navigation to now")
                let navStartMeasure = performance.getEntriesByType("measure")[0]
                startTimeMs = navStartMeasure.startTime
            }
        }
        export function measureStart(name: string) {
            if (enabled) performance.mark(`${name} start`)
        }
        export function measureEnd(name: string, params?: Map<string>) {
            if (enabled && performance.getEntriesByName(`${name} start`).length) {
                performance.mark(`${name} end`)
                performance.measure(`${name} elapsed`, `${name} start`, `${name} end`)
                let e = performance.getEntriesByName(`${name} elapsed`, "measure")
                if (e && e.length === 1) {
                    let measure = e[0]
                    let durMs = measure.duration
                    stats.durations.emit({ name, start: measure.startTime, duration: durMs, params });
                }
                performance.clearMarks(`${name} start`)
                performance.clearMarks(`${name} end`)
                performance.clearMeasures(`${name} elapsed`)
            }
        }
        export function report() {
            perfReportLogged = true;
            if (enabled) {
                const milestones: { [index: string]: number } = {};
                const durations: { [index: string]: number } = {};

                let report = `Performance Report:\n`
                report += `\n`
                report += `\tMilestones:\n`
                stats.milestones.forEach(({ milestone, time, params }) => {
                    let pretty = prettyStr(time)
                    report += `\t\t${milestone} @ ${pretty}`
                    for (let k of Object.keys(params || {})) {
                        report += `\n\t\t\t${k}: ${params[k]}`
                    }
                    report += `\n`
                    milestones[milestone] = time;
                });

                report += `\n`
                report += `\tMeasurements:\n`
                stats.durations.forEach(({ name, start, duration, params }) => {
                    let pretty = prettyStr(duration)
                    report += `\t\t${name} took ~ ${pretty}`
                    report += ` (${prettyStr(start)} - ${prettyStr(start + duration)})`
                    for (let k of Object.keys(params || {})) {
                        report += `\n\t\t\t${k}: ${params[k]}`
                    }
                    report += `\n`
                    durations[name] = duration;
                });

                console.log(report)
                enabled = false; // stop collecting milestones and measurements after report
                return { milestones, durations };
            }
            return undefined;
        }
        (function () {
            init()
            recordMilestone("first JS running")
        })()
    }

    export function initAnalyticsAsync() {
        if (isNativeApp() || shouldHideCookieBanner()) {
            initializeAppInsightsInternal(true);
            return;
        }

        if ((window as any).pxtSkipAnalyticsCookie) {
            initializeAppInsightsInternal(false);
            return;
        }

        initializeAppInsightsInternal(true);
    }

    export function aiTrackEvent(id: string, data?: any, measures?: any) {
        if (!eventLogger) {
            eventLogger = new TelemetryQueue((a, b, c) =>
                (window as any).appInsights.trackEvent({
                    name: a,
                    properties: b,
                    measurements: c,
                })
            );
        }
        eventLogger.track(id, data, measures);
    }

    export function aiTrackException(err: any, kind?: string, props?: any) {
        if (!exceptionLogger) {
            exceptionLogger = new TelemetryQueue((a, b, c) =>
                (window as any).appInsights.trackException({
                    exception: a,
                    properties: b ? { ...c, ["kind"]: b } : c,
                })
            );
        }
        exceptionLogger.track(err, kind, props);
    }

    export function initializeAppInsightsInternal(includeCookie = false) {
        try {
            const params = new URLSearchParams(window.location.search);
            if (params.has("partner")) {
                partnerName = params.get("partner");
            }
        }
        catch (e) {
            console.warn("Could not parse search string", e);
        }

        // loadAppInsights is defined in docfiles/tracking.html
        const loadAI = (window as any).loadAppInsights;
        if (loadAI) {
            isProduction = loadAI(includeCookie, telemetryInitializer);
            analyticsLoaded = true;
            queues.forEach(a => a.flush());
        }
    }

    function telemetryInitializer(envelope: any) {
        const pxtConfig = (window as any).pxtConfig;

        // App Insights automatically sends a page view event on setup, but we send our own later with additional properties.
        // This stops the automatic event from firing, so we don't end up with duplicate page view events.
        if (envelope.baseType == "PageviewData" && !envelope.baseData.properties) {
            return false;
        }

        if (envelope.baseType == "PageviewPerformanceData") {
            const pageName = envelope.baseData.name;
            envelope.baseData.name = window.location.origin;
            if (!envelope.baseData.properties) {
                envelope.baseData.properties = {};
            }
            envelope.baseData.properties.pageName = pageName;
            envelope.baseData.properties.pathName = window.location.pathname;
            // no url scrubbing for webapp (no share url, etc)
        }

        if (typeof pxtConfig === "undefined" || !pxtConfig) return true;

        const telemetryItem = envelope.baseData;
        telemetryItem.properties = telemetryItem.properties || {};
        telemetryItem.properties["target"] = pxtConfig.targetId;
        telemetryItem.properties["stage"] = (pxtConfig.relprefix || "/--").replace(/[^a-z]/ig, '')
        telemetryItem.properties["targetVersion"] = pxtConfig.targetVersion;
        telemetryItem.properties["pxtVersion"] = pxtConfig.pxtVersion;

        if (partnerName) {
            telemetryItem.properties["partner"] = partnerName;
        }

        const userAgent = navigator.userAgent.toLowerCase();
        const electronRegexResult = /\belectron\/(\d+\.\d+\.\d+.*?)(?: |$)/i.exec(userAgent); // Example navigator.userAgent: "Mozilla/5.0 Chrome/61.0.3163.100 Electron/2.0.0 Safari/537.36"
        if (electronRegexResult) {
            telemetryItem.properties["Electron"] = 1;
            telemetryItem.properties["ElectronVersion"] = electronRegexResult[1];
        }

        const pxtElectron = (window as any).pxtElectron;
        if (typeof pxtElectron !== "undefined") {
            telemetryItem.properties["PxtElectron"] = 1;
            telemetryItem.properties["ElectronVersion"] = pxtElectron.versions.electronVersion;
            telemetryItem.properties["ChromiumVersion"] = pxtElectron.versions.chromiumVersion;
            telemetryItem.properties["NodeVersion"] = pxtElectron.versions.nodeVersion;
            telemetryItem.properties["PxtElectronVersion"] = pxtElectron.versions.pxtElectronVersion;
            telemetryItem.properties["PxtCoreVersion"] = pxtElectron.versions.pxtCoreVersion;
            telemetryItem.properties["PxtTargetVersion"] = pxtElectron.versions.pxtTargetVersion;
            telemetryItem.properties["PxtElectronIsProd"] = pxtElectron.versions.isProd;
        }

        // Kiosk UWP info is appended to the user agent by the makecode-dotnet-apps/arcade-kiosk UWP app
        const kioskUwpRegexResult = /\((MakeCode Arcade Kiosk UWP)\/([\S]+)\/([\S]+)\)/i.exec(userAgent); // Example navigator.userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36 Edg/117.0.2045.60 (MakeCode Arcade Kiosk UWP/0.1.41.0/Windows.Xbox)"
        if (kioskUwpRegexResult) {
            telemetryItem.properties["KioskUwp"] = 1;
            telemetryItem.properties["KioskUwpVersion"] = kioskUwpRegexResult[2];
            telemetryItem.properties["KioskUwpPlatform"] = kioskUwpRegexResult[3];
        }

        // "cookie" does not actually correspond to whether or not we drop the cookie because we recently
        // switched to immediately dropping it rather than waiting. Instead, we maintain the legacy behavior
        // of only setting it to true for production sites where interactive consent has been obtained
        // so that we don't break legacy queries
        telemetryItem.properties["cookie"] = interactiveConsent && isProduction;

        return true;
    }

    export function setInteractiveConsent(enabled: boolean) {
        interactiveConsent = enabled;
    }

    /**
     * Checks for pxt-electron and Code Connection
     */
    function isNativeApp(): boolean {
        const hasWindow = typeof window !== "undefined";
        const isPxtElectron = hasWindow && !!(window as any).pxtElectron;
        const isCC = hasWindow && !!(window as any).ipcRenderer || /ipc=1/.test(location.hash) || /ipc=1/.test(location.search); // In WKWebview, ipcRenderer is injected later, so use the URL query
        return isPxtElectron || isCC;
    }
    /**
     * Checks whether we should hide the cookie banner
     */
    function shouldHideCookieBanner(): boolean {
        //We don't want a cookie notification when embedded in editor controllers, we'll use the url to determine that
        const noCookieBanner = isIFrame() && /nocookiebanner=1/i.test(window.location.href)
        return noCookieBanner;
    }
    function isIFrame(): boolean {
        try {
            return window && window.self !== window.top;
        } catch (e) {
            return false;
        }
    }
    /**
     * checks for sandbox
     */
    function isSandboxMode(): boolean {
        //This is restricted set from pxt.shell.isSandBoxMode and specific to share page
        //We don't want cookie notification in the share page
        const sandbox = /sandbox=1|#sandbox|#sandboxproject/i.test(window.location.href)
        return sandbox;
    }
}
