;"use strict";

import * as Nchan from '../js/medium/nchan.js';

import { Tokens } from './controller.js';
import { Messenger, UrlFactory, ControllerUrlFactory } from '../js/index.js';

const TrackedObjects = new WeakMap();

Object.defineProperties(TrackedObjects, {
    getValue: {
        enumerable: false,
        configurable: true,
        value(obj, propName) {
            const tracked = this.get(obj);
            const value = tracked ? tracked[propName] : undefined;
            this.mark(obj, 'get', propName, value);
            return value;
        }
    },
    setValue: {
        enumerable: false,
        configurable: true,
        value(obj, propName, value) {
            if (!this.has(obj)) this.set(obj, {});
            const tracked = this.get(obj);
            tracked[propName] = value;
            this.mark(obj, 'set', propName, value);
        }
    },
    mark: {
        enumerable: false,
        configurable: true,
        value(obj, type, name, args) {
            if (!obj._tracker) obj._tracker = [];
            obj._tracker.push({
                time: new Date(), type, name, args
            });
        }
    }
});


const createFilter = (filter) => {
    if (typeof filter === 'undefined') return x => true;
    if (typeof filter === 'string') return x => x === filter;
    if (filter instanceof RegExp) return x => filter.test(x);
    if (typeof filter.includes === 'function') return x => filter.includes(x);
    return x => true;
}


class ObjectTracker {

    constructor(obj) {
        this.target = obj;
        Object.defineProperties(this.target, {
            showHistory: {
                enumerable: false,
                configurable: true,
                value(filter) {
                    const data = this.getHistory(filter);
                    if (data) console.table(data.map(x => {
                        const copy = {...x};
                        copy.time = copy.time.toLocaleTimeString();
                        return copy;
                    }));
                }
            },
            getHistory: {
                enumerable: false,
                configurable: true,
                value(filter) {
                    if (!this._tracker) return;
                    let data = this._tracker;
                    if (filter && typeof filter === 'object') {
                        const filterType = createFilter(filter.type),
                              filterName = createFilter(filter.name);
                        data = data.filter(x => filterType(x.type) && filterName(x.name));
                    }
                    return data;
                }
            }
        })
    }

    trackMethods(...methods) {
        methods.forEach(method => this.trackMethod(method));
        return this;
    }

    trackProperties(...props) {
        props.forEach(prop => this.trackProperty(prop));
        return this;
    }

    trackMethod(methodName) {

        const method = this.target[methodName];
        if (typeof method !== 'function') return;

        Object.defineProperty(this.target, methodName, {
            enumerable: false,
            configurable: true,
            value(...args) {
                TrackedObjects.mark(this, 'call', methodName, args);
                const result = method.apply(this, args);
                TrackedObjects.mark(this, 'return', methodName, result);
                return result;
            }
        });

        return this;
    }

    trackProperty(propName) {
        Object.defineProperty(this.target, propName, {
            enumerable: true,
            configurable: true,
            get() {
                return TrackedObjects.getValue(this, propName);
            },
            set(value) {
                TrackedObjects.setValue(this, propName, value);
                return value;
            }
        });        

        return this;
    }
}

const enhanceClass = cls => new ObjectTracker(cls.prototype);


// For testing
Object.assign(window, {
    Nchan,
    UrlFactory,
    ControllerUrlFactory,
    Messenger,
    Tokens,
    enhanceClass,
    async testnchan(hook, channel, token=false) {
        const ctl = new AbortController();
        const options = { signal: ctl.signal };
        if (token === true) {
            options.authorization = await Tokens.generate(token);
        } else if (typeof token === 'string') {
            options.authorization = token;
        }
        Nchan.publish(
            new URL(`./messenger/test/pub/${channel || 'ch3'}`, location.href),
            JSON.stringify({ type: 'message', name: 'console', payload: `test ${hook}` }),
            options
        ).then(
            () => console.log(`[Test ${hook}] OK`),
            err => console.error(`[Test ${hook}]`, err)
        );
    },
    async testws(url, name, tokenData=null) {
        if (tokenData) {
            if (!url.endsWith('/')) url += '/';
            url += await Tokens.generate(tokenData);
        }
        const ws = new WebSocket(url);
        ws.onopen = () => console.info(`[${name}] opened`);
        ws.onclose = evt => console.info(`[${name}] closed with code ${evt.code}`);
        ws.onerror = evt => console.error(`[${name}]`, evt);
        ws.onmessage = evt => console.info(`[${name}] message: `, evt.data);
        return ws;
    }
});
