"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.objDiff = exports.EventEmitter = exports.StateEmitter = exports.StatusEmitter = exports.readAll = exports.ReadAllError = exports.BufferStream = exports.fromBase64 = exports.toBase64 = exports.ok = exports.startsWith = exports.Timeline = exports.Segment = exports.Sync = exports.swapRemoved = exports.getCharacterFiles = exports.watchCharacters = exports.characterData = exports.characterDir = exports.watchDir = exports.WatchedFile = exports.readDir = exports.appDir = exports.stopwatch = exports.Stopwatch = exports.RingBuffer = exports.mapToObject = exports.map = exports.objHas = exports.debugMode = void 0;
const fs = require("fs");
const perf_hooks_1 = require("perf_hooks");
const stream_1 = require("stream");
exports.debugMode = !!fs.existsSync('./debug');
console.log('`./debug` found?', exports.debugMode);
if (exports.debugMode) {
    console.log('debug - Enabled character file watching');
}
const objHas = (o, prop) => Object.prototype.hasOwnProperty.call(o, prop);
exports.objHas = objHas;
const map = (o) => {
    if (!o) {
        return new Map();
    }
    const keys = Object.keys(o);
    const m = new Map();
    for (const key of keys) {
        m.set(key, o[key]);
    }
    return m;
};
exports.map = map;
const mapToObject = (m) => {
    const o = {};
    for (const [k, v] of m) {
        o[k] = v;
    }
    return o;
};
exports.mapToObject = mapToObject;
class RingBuffer extends Float64Array {
    constructor() {
        super(...arguments);
        this.index = 0;
    }
    push(n) {
        this[this.index] = n;
        this.index = (this.index + 1) % this.length;
    }
    pop() {
        this.index = (this.index > 0 ? this.index : this.length) - 1;
        return this[this.index];
    }
    peek() {
        return this[(this.index > 0 ? this.index : this.length) - 1];
    }
}
exports.RingBuffer = RingBuffer;
class Stopwatch extends RingBuffer {
    constructor() {
        super(...arguments);
        this.lastTime = 0;
    }
    startLaps() {
        this.lastTime = perf_hooks_1.performance.now();
    }
    lap() {
        const t = perf_hooks_1.performance.now();
        this.push(t - this.lastTime);
        this.lastTime = t;
    }
    start() {
        this.push(perf_hooks_1.performance.now());
    }
    stop() {
        return perf_hooks_1.performance.now() - this.pop();
    }
}
exports.Stopwatch = Stopwatch;
exports.stopwatch = new Stopwatch(64);
exports.appDir = `${process.cwd()}/`;
const readDir = (dir) => {
    const dirFiles = fs.readdirSync(exports.appDir + dir);
    const fileMap = new Map();
    if (!dirFiles) {
        console.error('Falsy dirFiles:', dirFiles);
    }
    for (let i = 0; i < dirFiles.length; i++) {
        const f = dirFiles[i];
        fileMap.set(f, fs.readFileSync(`${exports.appDir + dir}/${f}`, 'utf8'));
    }
    return fileMap;
};
exports.readDir = readDir;
class WatchedFile {
    constructor(file) {
        this.watchers = [];
        this.filename = file;
        this.content = fs.readFileSync(file, 'utf8');
        if (!exports.debugMode) {
            return;
        }
        try {
            fs.watch(file, (_event, filename) => {
                const content = fs.readFileSync(file, 'utf8');
                console.log('file updated', filename);
                if (!content) {
                    console.log('No content, ignoring');
                    return;
                }
                if (this.content === content) {
                    console.log('Content unchanged, ignoring');
                    return;
                }
                this.content = content;
                for (let i = 0; i < this.watchers.length; i++) {
                    console.log('dispatched to watcher', i);
                    this.watchers[i](this);
                }
            });
        }
        catch (e) {
            console.warn('unable to watch file:', e);
        }
    }
    watch(callback) {
        this.watchers.push(callback);
    }
    unwatch(callback) {
        if (!this.watchers.includes(callback)) {
            return;
        }
        this.watchers.splice(this.watchers.indexOf(callback), 1);
    }
}
exports.WatchedFile = WatchedFile;
const watchDir = (dir) => {
    const dirFiles = fs.readdirSync(exports.appDir + dir);
    const fileMap = new Map();
    if (!dirFiles) {
        console.error('Falsy dirFiles:', dirFiles);
    }
    for (let i = 0; i < dirFiles.length; i++) {
        const f = dirFiles[i];
        fileMap.set(f, new WatchedFile(`${exports.appDir + dir}/${f}`));
    }
    return fileMap;
};
exports.watchDir = watchDir;
exports.characterDir = 'app/characters/data';
exports.characterData = exports.watchDir(exports.characterDir);
const watchers = [];
const watchCharacters = (callback) => {
    console.log('character watching enabled');
    watchers.push(callback);
};
exports.watchCharacters = watchCharacters;
const getCharacterFiles = (name, ...files) => {
    const f = [];
    const cb = () => {
        for (let i = 0; i < watchers.length; i++) {
            watchers[i](name);
        }
    };
    for (let i = 0; i < files.length; i++) {
        if (!exports.characterData.has(files[i])) {
            console.error('Oh no! A character file was not found:', files[i]);
        }
        const w = exports.characterData.get(files[i]);
        f.push(w);
        w.watch(cb);
    }
    return f;
};
exports.getCharacterFiles = getCharacterFiles;
const swapRemoved = (from, to, match) => {
    const l = from.length;
    let r = 0;
    for (let i = 0; i < l; i++) {
        if (from[i].removed === match) {
            to.push(from[i]);
            r++;
            from[i].present = !match;
        }
        else {
            if (r > 0) {
                from[i - r] = from[i];
            }
        }
    }
    from.length = from.length - r;
};
exports.swapRemoved = swapRemoved;
let maxSize = 0;
let loadSize = 0;
let loadGroups = 3;
let resolveLoading = null;
const doneLoading = (size = 0) => {
    loadSize = loadSize - size;
    if (loadGroups === 0 && loadSize <= 0) {
        resolveLoading(null);
    }
};
exports.Sync = {
    loading: {
        promise: new Promise(res => {
            resolveLoading = res;
        }),
        push: (p, size = 0) => {
            loadSize = loadSize + size;
            maxSize = maxSize + size;
            p.then(() => doneLoading(size));
            p instanceof Promise
                && p.catch(err => {
                    console.warn('Error loading asset:', err);
                    doneLoading(size);
                });
        },
        done: () => {
            loadGroups = loadGroups - 1;
            doneLoading(0);
        },
    },
};
class Segment {
    constructor() {
        this.start = 0;
        this.end = 0;
        this.handler = null;
    }
}
exports.Segment = Segment;
class Timeline {
    constructor() {
        this.referenceTime = 0;
        this.referencePoint = 0;
        this.duration = 0;
        this.speed = 1;
        this.time = 0;
        this.loop = false;
        this.segments = null;
    }
    reset(t) {
        this.referenceTime = t;
        this.referencePoint = 0;
        this.speed = 1;
    }
    update(t) {
        const dest = (t - this.referenceTime) * this.speed;
        this.time = dest;
    }
    find(t) {
        const found = [];
        for (let i = 0; i < this.segments.length; i++) {
            const s = this.segments[i];
            if (s.start < t && s.end > t) {
                found.push(s);
            }
        }
        return found;
    }
    sweep(from, to) {
        const found = [];
        for (let i = 0; i < this.segments.length; i++) {
            const s = this.segments[i];
            if (s.start < to && s.end > from) {
                found.push(s);
            }
        }
        return found;
    }
}
exports.Timeline = Timeline;
const startsWith = (str) => (item) => item.substr(0, str.length).toLowerCase() === str;
exports.startsWith = startsWith;
exports.ok = new Set(['true', '1', 'yes', 'on']);
const toBase64 = (b) => Buffer.from(b).toString('base64');
exports.toBase64 = toBase64;
const fromBase64 = (s) => Buffer.from(s, 'base64');
exports.fromBase64 = fromBase64;
class BufferStream extends stream_1.Readable {
    _read() { }
    constructor(b) {
        super();
        this.push(b);
        this.push(null);
    }
}
exports.BufferStream = BufferStream;
class ReadAllError extends Error {
    constructor(buffer, err) {
        super();
        this.buffer = buffer;
        this.err = err;
        this.message = err.message;
        this.stack = err.stack;
    }
}
exports.ReadAllError = ReadAllError;
const readAll = (r) => new Promise((resolve, reject) => {
    const chunks = [];
    r.on('data', chunk => chunks.push(chunk));
    r.on('end', () => resolve(Buffer.concat(chunks)));
    r.on('error', err => {
        reject(new ReadAllError(Buffer.concat(chunks), err));
    });
});
exports.readAll = readAll;
class StatusEmitter {
    constructor(val, data = null) {
        this.data = null;
        this.listeners = new Set();
        this.statusListeners = new Map();
        this.status = val;
        this.data = data;
    }
    listen(callback, status = null) {
        let cb = callback;
        if (status !== null) {
            cb = s => status === s && callback(status);
            this.statusListeners.set(callback, cb);
        }
        this.listeners.add(cb);
        cb(this.status, this.data);
    }
    wait(status = null) {
        return new Promise(resolve => {
            const callback = (s, data) => {
                this.listeners.delete(callback);
                resolve([s, data]);
            };
            let cb = callback;
            if (status !== null) {
                if (this.status === status) {
                    return;
                }
                cb = (s, data) => {
                    if (status === s) {
                        resolve([this.status, this.data]);
                        return;
                    }
                    this.statusListeners.delete(cb);
                    resolve([status, data]);
                };
                this.statusListeners.set(callback, cb);
            }
            this.listeners.add(cb);
        });
    }
    remove(callback) {
        if (this.statusListeners.has(callback)) {
            this.listeners.delete(this.statusListeners.get(callback));
            this.statusListeners.delete(callback);
        }
        else {
            this.listeners.delete(callback);
        }
    }
    clear() {
        this.listeners.clear();
        this.statusListeners.clear();
    }
    set(status, data = null) {
        this.status = status;
        this.data = data;
        for (const l of this.listeners) {
            l(status, data);
        }
    }
}
exports.StatusEmitter = StatusEmitter;
class StateEmitter extends Set {
    constructor(owner) {
        super();
        this.owner = owner;
        this.listeners = new Set();
        this.statusListeners = new Map();
    }
    listen(callback, status = null, state = -1) {
        let cb = callback;
        if (status !== null) {
            cb = (o, _status, _state) => (state === -1 || _state === state)
                && _status === status
                && callback(o, _status, _state, this);
            this.statusListeners.set(callback, cb);
            this.listeners.add(cb);
            if (this.has(status)) {
                cb(this.owner, status, 1, this);
            }
            return;
        }
        this.listeners.add(cb);
    }
    remove(callback) {
        if (this.statusListeners.has(callback)) {
            this.listeners.delete(this.statusListeners.get(callback));
            this.statusListeners.delete(callback);
        }
        else {
            this.listeners.delete(callback);
        }
    }
    clear() {
        this.clearHandlers();
        super.clear();
    }
    clearHandlers() {
        this.listeners.clear();
        this.statusListeners.clear();
    }
    add(status) {
        if (this.has(status)) {
            return this;
        }
        super.add(status);
        for (const l of this.listeners) {
            l(this.owner, status, 1, this);
        }
        return this;
    }
    delete(status) {
        if (!this.has(status)) {
            return false;
        }
        super.delete(status);
        for (const l of this.listeners) {
            l(this.owner, status, 0, this);
        }
        return true;
    }
}
exports.StateEmitter = StateEmitter;
class EventEmitter {
    constructor() {
        this.listeners = new Map();
    }
    listen(event, callback) {
        if (!this.listeners.has(event)) {
            this.listeners.set(event, new Set());
        }
        this.listeners.get(event).add(callback);
    }
    remove(event, callback) {
        if (!this.listeners.has(event)) {
            return;
        }
        this.listeners.get(event).delete(callback);
        if (this.listeners.get(event).size === 0) {
            this.listeners.delete(event);
        }
    }
    clear() {
        this.listeners.clear();
    }
    emit(event, data = null) {
        if (!this.listeners.has(event)) {
            return;
        }
        for (const l of this.listeners.get(event)) {
            l(data, event);
        }
    }
}
exports.EventEmitter = EventEmitter;
const objDiff = (a, b) => {
    const added = [];
    const removed = [];
    for (const key in b) {
        if (!exports.objHas(b, key)) {
            continue;
        }
        if (!exports.objHas(a, key)) {
            added.push(key);
        }
    }
    for (const key in a) {
        if (!exports.objHas(a, key)) {
            continue;
        }
        if (!exports.objHas(b, key)) {
            removed.push(key);
        }
    }
    return [added, removed];
};
exports.objDiff = objDiff;
//# sourceMappingURL=utils.js.map