const fs = require('fs');
const path = require('path');

const actions = {
    CREATED: 0,
    DELETED: 1,
    MODIFIED: 2,
    RENAMED: 3,
};

class FilePoller {
    constructor(watchPath, eventCallback, errorCallback, debounceMS) {
        this.watchPath = watchPath;
        this.eventCallback = eventCallback;
        this.errorCallback = errorCallback;
        this.debounceMS = debounceMS;

        this.fileStatus = null;
        this.filePollerInterval = 0;

        const directory = path.dirname(watchPath);
        const file = path.basename(watchPath);

        this.getStatus = async () => {
            try {
                const status = await fs.promises.stat(watchPath);

                if (this.fileStatus === null) {
                    this.fileStatus = status;
                    eventCallback([{ action: actions.CREATED, directory, file }]);
                } else if (
                    status.mtime - this.fileStatus.mtime !== 0 ||
                    status.ctime - this.fileStatus.ctime !== 0
                ) {
                    this.fileStatus = status;
                    eventCallback([{ action: actions.MODIFIED, directory, file }]);
                }
            } catch (e) {
                if (this.fileStatus !== null) {
                    this.fileStatus = null;
                    eventCallback([{ action: actions.DELETED, directory, file }]);
                }
            }
        };
    }

    async start() {
        try {
            this.fileStatus = await fs.promises.stat(watchPath);
        } catch (e) {
            this.fileStatus = null;
        }

        this.filePollerInterval = setInterval(this.getStatus, this.debounceMS);
    }

    async stop() {
        clearInterval(this.filePollerInterval);
    }

    pause() {
        this.stop();
    }

    resume() {
        this.start();
    }
}

const eventListeners = Object.create(null);
const errorListeners = Object.create(null);

on('sdk:fileWatcherEvents', (watcherId, events) => {
    const listener = eventListeners[watcherId];
    if (!listener) {
        return;
    }

    listener(events);
});

on('sdk:fileWatcherError', (watcherId, error) => {
    const listener = errorListeners[watcherId];
    if (!listener) {
        return;
    }

    listener(error);
});

class FXWatcher {
    constructor(watchPath, eventCallback, errorCallback) {
        this.watchPath = watchPath;
        this.eventCallback = eventCallback;
        this.errorCallback = errorCallback;

        this.paused = true;

        const handleEvents = (events) => {
            if (this.paused) {
                return;
            }

            const normalizedEvents = events.map(([action, fromDirectory, fromFile, toDirectory, toFile]) => {
                if (action === actions.RENAMED) {
                    return {
                        action,
                        directory: fromDirectory,
                        oldFile: fromFile,
                        newDirectory: toDirectory,
                        newFile: toFile,
                    };
                }

                return {
                    action,
                    directory: fromDirectory,
                    file: fromFile,
                };
            });

            eventCallback(normalizedEvents);
        };
        const handleError = (error) => errorCallback(new Error(error));

        const requestId = Math.abs(Math.random() * 0xffffffff | 0).toString(16);

        const setWatcherId = (requestIdCandidate, watcherId) => {
            if (requestId !== requestIdCandidate) {
                return;
            }

            eventListeners[watcherId] = handleEvents;
            errorListeners[watcherId] = handleError;

            RemoveEventHandler('sdk:fileWatcherId', setWatcherId);

            this.watcherId = watcherId;
        };

        on('sdk:fileWatcherId', setWatcherId);
        emit('sdk:startFileWatcher', watchPath, requestId);
    }

    start() {
        this.paused = false;

        return Promise.resolve();
    }

    stop() {
        emit('sdk:stopFileWatcher', this.watcherId);

        delete eventListeners[this.watcherId];
        delete errorListeners[this.watcherId];

        return Promise.resolve();
    }

    pause() {
        this.paused = true;
    }

    resume() {
        this.paused = false;
    }
}

async function nsfw(watchPath, eventCallback, options = {}) {
    debounceMS = options.debounceMS || 500;
    errorCallback = options.errorCallback || ((nsfwError) => { throw nsfwError; });

    if (Number.isInteger(debounceMS)) {
        if (debounceMS < 1) {
            throw new Error('Minimum debounce is 1ms.');
        }
    } else {
        throw new Error('debounceMS must be an integer.');
    }

    if (path.isAbsolute(watchPath)) {
        throw new Error('Path to watch must be an absolute path.');
    }

    let stat;
    try {
        stat = await fs.promises.stat(watchPath);
    } catch (e) {
        throw new Error('Path must be a valid path to a file or a directory.');
    }

    if (stat.isDirectory()) {
        return new FXWatcher(watchPath, eventCallback, errorCallback);
    } else if (stat.isFile()) {
        return new FilePoller(watchPath, eventCallback, errorCallback, debounceMS);
    } else {
        throw new Error('Path must be a valid path to a file or a directory');
    }
}

nsfw.actions = actions;

module.exports = nsfw;
