export const Tokens = {
    channels: new Set(),
    controlers: new Set(),
    async list(token) {
        const url = "./token.php" + (token ? `?token=${token}` : '');
        const response = await fetch(url, { credentials: 'include' });
        return await response.text();
    },
    async generate(payload) {
        return await fetch('./token.php', {
            method: 'POST',
            headers: { 'Content-Type': 'text/json' },
            credentials: 'include',
            body: JSON.stringify(payload)
        }).then(res => res.text());
    },
    async channel({group, pub, sub} = {}) {
        const data = { group };
        if (pub) data.pub = Array.isArray(pub) ? pub.join(',') : pub;
        if (sub) data.sub = Array.isArray(sub) ? sub.join(',') : sub;
        const token = await this.generate(data);
        this.channels.add(token);
        return token;
    },
    async controller({ group, ctl } = {}) {
        const data = { group };
        if (Array.isArray(ctl)) {
            data.ctl = ctl.join(',');
        } else if (typeof ctl === 'string') {
            data.ctl = ctl;
        } else {
            data.ctl = '';
        }
        const token = await this.generate(data);
        this.controlers.add(token);
        return token;
    }
};

export class Controller {

    constructor(form) {
        this.root = form;
        this.output = form.querySelector('.output');
        form.nchanctl = this;

        this.onopen = null;
        this.onclose = null;
        this.onmessage = null;
        this.onrequest = null;

        this.errors = [];

        let btn;

        this.connectBtn = this.root.querySelector('input[name=connect]');
        this.connectBtn.addEventListener('click', evt => {
            let action;
            if (evt.target.value === 'Connect') {
                evt.target.value = 'Close';
                action = this.onopen;
            } else {
                evt.target.value = 'Connect';
                action = this.onclose;
            }
            if (typeof action === 'function') Promise.resolve()
                .then(action)
                .catch(console.error.bind(console,'Action has failed:'));
        });
        this.root.querySelector('input[name=clear]').addEventListener('click', evt => {
            this.output.textContent = '';
        });

        btn = this.root.querySelector('input[name=message]');
        if (btn) btn.addEventListener('click', evt => {
            if (typeof this.onmessage === 'function')
                this.onmessage(this.name, this.payload, this.channels)
        });

        btn = this.root.querySelector('input[name=request]');
        if (btn) btn.addEventListener('click', evt => {
            if (typeof this.onrequest === 'function')
                this.onrequest(this.name, this.payload, this.channels)
        });

        btn = this.root.querySelector('input[name=post]');
        if (btn) btn.addEventListener('click', evt => {
            fetch('./post.php', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({
                    'name': this.name,
                    'payload': this.payload,
                    'channels': this.channels
                })
            }).then(async response => {
                if (response.ok) {
                    return { type: 'message', payload: await response.text() };
                } else {
                    return { type: 'error', payload: `HTTP ${response.status}: ${response.statusText}` };
                }
            }).then(data => {
                console.info('Post result:', data);
                this.post({ name: 'post result', channel: 'ctl', ...data });
            });
        });
    }

    set status(value) {
        this.root.querySelector('.status-content').textContent = value;
        this.connectBtn.value = value.startsWith('clo') ? 'Connect' : 'Close';
    }

    post({ timestamp, name, type, channel, payload } = {}) {
        const cnt = document.createElement('div');
        cnt.className = 'message';
        const header = document.createElement('div');
        header.className = type || 'message';
        header.textContent = '[' + (timestamp ? new Date(timestamp*1000) : new Date()).toLocaleTimeString() + `] ${channel || 'internal'} : ${name}`;
        cnt.append(header);
        if (typeof payload !== 'undefined') {
            try {
                if (typeof payload === 'string') payload = JSON.parse(payload);
                cnt.append(JSON.stringify(payload, null, 4));
            } catch(err) {
                cnt.append(payload);
            }
        }
        this.output.prepend(cnt);
    }

    handleError(error, name='error') {
        let payload = '';
        if (error instanceof Error || error instanceof DOMException) {
            payload = `[${error.name}] ${error.message}`;
        }
        this.post({ type: 'error', name, payload })
        this.errors.push(error);
        console.error(error);
    }

    get type() {
        const checked = this.root.querySelector('input[type=radio]:checked');
        return checked ? checked.value : 'message';
    }

    get channels() {
        const checked = [...this.root.querySelectorAll('input[type=checkbox]:checked')].map(x => x.name);
        return checked.length ? checked : null;
    }

    get payload() {
        const value = this.root.querySelector('textarea').value;
        try {
            return JSON.parse(value);
        } catch(err) {
            return value;
        }
    }

    get name() {
        return this.root.querySelector('input[name=name]').value;
    }
}


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;
    }
}

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