/**
 * ChirpOS - Kernel Module
 * @author Cawdabra
 * @license MIT
 */
"use strict";

require("../globals.js");
const os = require("_os");
const log = require("_log");
const io = require("_io");

// Namespace
var kernel = {};

/**
 * Kernel class
 * @class
 */
kernel.Kernel = function() {
    this.loggerInstance = new log.Logger(log.LogLevel.DEBUG);
    log.addLoggerMethod(this);

    this._fileSystem = new io.FileSystem();
    this._fileLocks = {
        0: {}       // Kernel
    };
};

/**
 * Returns the file system
 * @return {io.FileSystem}
 */
kernel.Kernel.prototype.getFileSystem = function() {
    return this._fileSystem;
};

/**
 * Returns all locks on a file and its parents
 * @param {io.File} file
 * @return {object}
 */
kernel.Kernel.prototype.findFileLocks = function(file) {
    var path = file.getPath();

    // Cache the node paths
    var file_path = file.getPath();
    var paths = [];
    while (file_path !== "" && file_path !== os.path.DIR_SEPARATOR) {
        paths.push(file_path);
        file_path = os.path.getDirName(file_path);
    }

    if (file_path === os.path.DIR_SEPARATOR) {
        // Need to add root manually if it has a blank name
        paths.push(os.path.DIR_SEPARATOR);
    }

    var locks = {};
    var pid_key, path_iter;
    for (pid_key in this._fileLocks) {
        if (this._fileLocks.hasOwnProperty(pid_key)) {
            for (path_iter=0; path_iter<paths.length; ++path_iter) {
                path = paths[path_iter];
                if (this._fileLocks[pid_key].hasOwnProperty(path)) {
                    if (locks.hasOwnProperty(pid_key)) {
                        locks[pid_key][path] = this._fileLocks[pid_key][path];
                    }
                    else {
                        locks[pid_key] = {};
                        locks[pid_key][path] = this._fileLocks[pid_key][path];
                    }
                }
            }
        }
    }

    return locks;
};

/**
 * Checks for any read locks on a file or its parents
 * @param {number} pid The process ID to exclude from the search
 * @param {io.File} file
 * @return {boolean}
 */
kernel.Kernel.prototype.canReadFrom = function(pid, file) {
    // Keys get converted to strings automatically during assignment
    pid = pid.toString();

    var locks = this.findFileLocks(file);
    var pid_key, path_key;
    for (pid_key in locks) {
        if (pid_key !== pid && locks.hasOwnProperty(pid_key)) {
            for (path_key in locks[pid_key]) {
                if (locks[pid_key].hasOwnProperty(path_key) && locks[pid_key][path_key].read) {
                    return false;
                }
            }
        }
    }

    return true;
};

/**
 * Returns if a file or directory is read locked without checking its parents
 * @param {number} pid The process ID to exclude from the search
 * @param {io.File} file
 * @return {boolean}
 */
kernel.Kernel.prototype.canReadFromSingle = function(pid, file) {
    // Keys get converted to strings automatically during assignment
    pid = pid.toString();
    var path = file.getPath();
    var pid_key;
    for (pid_key in this._fileLocks) {
        if (pid_key !== pid && this._fileLocks.hasOwnProperty(pid_key)) {
            if (this._fileLocks[pid_key][path].read) {
                return false;
            }
        }
    }

    return true;
};

/**
 * Checks for any write locks on a file or its parents
 * @param {number} pid The process ID to exclude from the search
 * @param {io.File} file
 * @return {boolean}
 */
kernel.Kernel.prototype.canWriteTo = function(pid, file) {
    // Keys get converted to strings automatically during assignment
    pid = pid.toString();

    var locks = this.findFileLocks(file);
    var pid_key, path_key;
    for (pid_key in locks) {
        if (pid_key !== pid && locks.hasOwnProperty(pid_key)) {
            for (path_key in locks[pid_key]) {
                if (locks[pid_key].hasOwnProperty(path_key) && locks[pid_key][path_key].write) {
                    return false;
                }
            }
        }
    }

    return true;
};

/**
 * Checks for file locks on all child nodes of a directory
 * NOTE: Also checks for read or write locks on the parent directories, going all the way up to root
 * @param {number} pid Process ID
 * @param {io.Directory} directory
 * @param {array} [locked_files] Used internally on recursion
 * @return {StatusCode}
 */
kernel.Kernel.prototype.canAlterDirectory = function(pid, directory, locked_files) {
    // Compress all of the file locks into one searchable array of paths
    if (!locked_files) {
        locked_files = [];
        var pid_key, path_key;
        for (pid_key in this._fileLocks) {
            if (this._fileLocks.hasOwnProperty(pid_key)) {
                for (path_key in this._fileLocks[pid_key]) {
                    if (this._fileLocks[pid_key].hasOwnProperty(path_key)
                    && (this._fileLocks[pid_key][path_key].read || this._fileLocks[pid_key][path_key].write)
                    && locked_files.indexOf(path_key) === -1) {
                        locked_files.push(path_key);
                    }
                }
            }
        }

        // Check the parents of this directory
        var parent_paths = [];
        var path = os.path.getDirName(directory.getPath());;
        while (path !== "" || path !== os.path.DIR_SEPARATOR) {
            parent_paths.push(path);
            path = os.path.getDirName(path);
        }

        if (path === os.path.DIR_SEPARATOR) {
            if (locked_files.indexOf(path) > -1) {
                return StatusCode.FILE_IS_LOCKED;
            }
        }

        var i;
        for (i=0; i<parent_paths.length; ++i) {
            if (locked_files.indexOf(parent_paths[i]) > -1) {
                return StatusCode.FILE_IS_LOCKED;
            }
        }
    }

    if (locked_files.indexOf(directory.getPath()) > -1) {
        return false;
    }

    var children = directory.getChildren();
    var node_key;
    for (node_key in children) {
        if (children.hasOwnProperty(node_key)) {
            if (locked_files.indexOf(node_key.getPath()) > -1) {
                return StatusCode.FILE_IS_LOCKED;
            }

            // Permission denied
            if (children[node_key].hasFlag(io.FileFlag.SYSTEM)) {
                return StatusCode.PERMISSION_DENIED;
            }

            if (children[node_key] instanceof io.Directory) {
                status =  this.canAlterDirectory(pid, children[node_key], locked_files);
                if (status !== StatusCode.SUCCESS) {
                    return status;
                }
            }
        }
    }

    return StatusCode.SUCCESS;
};

/**
 * Returns if a file or directory is write locked without checking its parents
 * @param {number} pid The process ID to exclude from the search
 * @param {io.File} file
 * @return {boolean}
 */
kernel.Kernel.prototype.canWriteToSingle = function(pid, file) {
    // Keys get converted to strings automatically during assignment
    pid = pid.toString();
    var path = file.getPath();
    var pid_key, node;
    for (pid_key in this._fileLocks) {
        if (pid_key !== pid && this._fileLocks.hasOwnProperty(pid_key)) {
            if (this._fileLocks[pid][path].write) {
                return false;
            }
        }
    }

    return true;
};

/**
 * Attempts to read lock a file
 * @param {number} pid
 * @param {io.File} file
 * @param {boolean} lock
 * @return {StatusCode}
 */
kernel.Kernel.prototype.readLockFile = function(pid, file, lock) {
    // Keys get converted to strings automatically during assignment
    pid = pid.toString();

    var locks = this.findFileLocks(file);
    var pid_key, path_key;
    for (pid_key in locks) {
        if (pid_key !== pid && locks.hasOwnProperty(pid_key)) {
            for (path_key in locks[pid_key]) {
                if (locks[pid_key].hasOwnProperty(path_key) && locks[pid_key][path_key].read) {
                    return StatusCode.FILE_IS_READ_LOCKED;
                }
            }
        }
    }

    var path = file.getPath();
    if (locks.hasOwnProperty(pid)) {
        if (locks[pid].hasOwnProperty(path)) {
            locks[pid][path].read = lock;
        }
        else {
            // New entry
            locks[pid][path] = {
                read: lock,
                write: false
            };
        }
        this.logger().info("Kernel", "Process #" + pid + " updated lock for: '" + path + "', read=" + lock + ", write=" + locks[pid][path].write);
    }
    else {
        // New entry
        var path = file.getPath();
        this._fileLocks[pid] = {};
        this._fileLocks[pid][path] = {
            read: lock,
            write: false
        };
        this.logger().info("Kernel", "Process #" + pid + " obtained lock for: '" + path + "', read=" + lock + ", write=" + false);
    }

    return StatusCode.SUCCESS;
};

/**
 * Attempts to write lock a file
 * @param {number} pid
 * @param {io.FIle} file
 * @param {boolean} lock
 * @return {StatusCode}
 */
kernel.Kernel.prototype.writeLockFile = function(pid, file, lock) {
    // Keys get converted to strings automatically during assignment
    pid = pid.toString();

    var locks = this.findFileLocks(file);
    var pid_key, path_key;
    for (pid_key in locks) {
        if (pid_key !== pid && locks.hasOwnProperty(pid_key)) {
            for (path_key in locks[pid_key]) {
                if (locks[pid_key].hasOwnProperty(path_key) && locks[pid_key][path_key].write) {
                    return StatusCode.FILE_IS_WRITE_LOCKED;
                }
            }
        }
    }

    var path = file.getPath();
    if (locks.hasOwnProperty(pid)) {
        if (locks[pid].hasOwnProperty(path)) {
            locks[pid][path].write = lock;
        }
        else {
            // New entry
            locks[pid][path] = {
                read: false,
                write: lock
            };
        }
        this.logger().info("Kernel", "Process #" + pid + " updated lock for: '" + path + "', read=" + locks[pid][path].read + ", write=" + lock);
    }
    else {
        // New entry
        this._fileLocks[pid] = {};
        this._fileLocks[pid][path] = {
            read: false,
            write: lock
        };
        this.logger().info("Kernel", "Process #" + pid + " obtained lock for: '" + path + "', read=" + false + ", write=" + lock);
    }

    return StatusCode.SUCCESS;
};

/**
 * Deletes a file lock
 * @param {number} pid
 * @param {io.File} file
 * @return {StatusCode}
 */
kernel.Kernel.prototype.deleteFileLock = function(pid, file) {
    var path = file.getPath();
    if (!this._fileLocks.hasOwnProperty(pid) || !this._fileLocks[pid].hasOwnProperty(path)) {
        return StatusCode.FILE_LOCK_NOT_FOUND;
    }

    this.logger().info("Deleting lock for process: #" + pid + ", file='" + path + "'");
    delete this._fileLocks[pid][path];

    // Delete the entire object if there's no file locks in it
    var file_key;
    var delete_object = true;
    for (file_key in this._fileLocks[pid]) {
        if (this._fileLocks[pid].hasOwnProperty(file_key)) {
            delete_object = false;
            break;
        }
    }

    if (delete_object) {
        this.logger().info("Process #" + pid + " has no file locks. Deleting object.");
        delete this._fileLocks[pid];
    }

    return StatusCode.SUCCESS;
}

/**
 * Creates a new file in the file system
 * @param {number} pid Process ID
 * @param {string} path
 * @param {number} flags
 * @param {boolean} user_mode
 * @return {StatusObject}
 */
kernel.Kernel.prototype.createFile = function(pid, path, flags, user_mode) {
    var dir_name = os.path.getDirName(path);
    var status = this._fileSystem.getNodeFromPath(dir_name);
    if (!status.isSuccess()) {
        // Parent directory not found
        return status;
    }

    var parent = status.result;
    if (!(parent instanceof io.Directory)) {
        // Parent is not a directory
        return new StatusObject(StatusCode.NODE_IS_NOT_DIRECTORY, "Node '" + dir_name + "' is not a directory");
    }

    if (user_mode && parent.hasFlag(io.FileFlag.SYSTEM)) {
        return new StatusObject(StatusCode.PERMISSION_DENIED);
    }

    if (user_mode && !this.canWriteTo(pid, parent)) {
        return new StatusObject(StatusCode.FILE_IS_WRITE_LOCKED, "Parent directory '" + dir_name + "' is write locked");
    }

    var file = new io.File(parent, new io.FileSystemNodeInfo(os.path.getBaseName(path), 0, 0, 0, flags));
    status = parent.setNode(file, user_mode);
    if (status !== StatusCode.SUCCESS) {
        return new StatusObject(status);
    }

    this.logger().info("Kernel", "Process #" + pid + " created file: '" + path + "'");
    return new StatusObject(StatusCode.SUCCESS, file);;
};

/**
 * Attempts to create as many directories as necessary to reach the end of the path
 * @param {number} pid Process ID
 * @param {string} path
 * @param {number} flags
 * @param {boolean} user_mode User mode induces permission and lock checks
 * @return {StatusObject}
 */
kernel.Kernel.prototype.makeDirs = function(pid, path, flags, user_mode) {
    this.logger().info("Kernel", "Creating directories for path: '" + path + "'");
    var split = path.split(os.path.DIR_SEPARATOR);
    var i, dir, status;
    var parent = this._fileSystem.getRoot();
    for (i=1; i<split.length; ++i) {
        // Traverse the path
        status = this._fileSystem.getNodeFromPath(split.slice(0, i + 1).join(os.path.DIR_SEPARATOR));
        if (status.code === StatusCode.SUCCESS) {
            parent = status.result;
            if (!(parent instanceof io.Directory)) {
                // Parent is not a directory
                return new StatusObject(StatusCode.NODE_IS_NOT_DIRECTORY, "Node '" + split[i] + "' is not a directory");
            }
        }
        else {
            // Not found
            this.logger().debug("Kernel", "Creating directory: '" + split[i] + "'");
            if (user_mode && !this.canWriteTo(pid, parent)) {
                return new StatusObject(StatusCode.FILE_IS_WRITE_LOCKED);
            }

            dir = new io.Directory(parent, new io.FileSystemNodeInfo(split[i], 0, 0, 0, flags));
            status = parent.setNode(dir, user_mode);
            if (status !== StatusCode.SUCCESS) {
                // Cannot continue any further
                return new StatusObject(status);
            }

            parent = dir;
        }
    }

    return new StatusObject(StatusCode.SUCCESS, dir);
};

/**
 * Returns a file stream to a file
 * @param {number} pid Process ID
 * @param {string} path
 * @param {string} mode "r" = read, "w" = write, "a" = append
 * @param {boolean} user_mode User mode induces permission checks
 * @return {StatusObject}
 */
kernel.Kernel.prototype.openFile = function(pid, path, mode, user_mode) {
    this.logger().info("Process #" + pid + " attempting to open file '" + path + "'");
    var status = this._fileSystem.getNodeFromPath(path);
    if (!status.isSuccess()) {
        if ((mode.indexOf("w") > -1 || mode.indexOf("a") > -1) && status.code === StatusCode.FILE_NOT_FOUND) {
            // Create new file
            status = this.createFile(pid, path, 0, user_mode);
            if (!status.isSuccess()) {
                return status;
            }
        }
        else {
            return status;
        }
    }

    var file = status.result;
    if (user_mode) {
        if (file.hasFlag(io.FileFlag.SYSTEM)) {
            return new StatusObject(StatusCode.PERMISSION_DENIED);
        }

        if (mode.indexOf("r") > -1 && !this.canReadFrom(pid, file)) {
            return new StatusObject(StatusCode.FILE_IS_READ_LOCKED);
        }

        if ((mode.indexOf("w") > -1 || mode.indexOf("a") > -1) && !this.canWriteTo(pid, file)) {
            return new StatusObject(StatusCode.FILE_IS_WRITE_LOCKED);
        }
    }

    return new StatusObject(StatusCode.SUCCESS, new io.FileStream(this, pid, file, mode));
};

/**
 * Copies a a file or directory to a destination
 * @param {number} pid Process ID
 * @param {string} source_path
 * @param {stirng} dest_path
 * @param {boolean} user_mode User mode induces permissions and lock checks
 * @return {StatusObject}
 */
kernel.Kernel.prototype.copyFile = function(pid, source_path, dest_path, user_mode) {
    this.logger().info("Kernel", "Copying '" + source_path + "' to '" + dest_path + "'");

    var status = this._fileSystem.getNodeFromPath(source_path);
    if (!status.isSuccess()) {
        status.result = "Source file '" + source_path + "' not found";
        return status;
    }
    var source = status.result;

    var dest_dir_path = os.path.getDirName(dest_path);
    status = this._fileSystem.getNodeFromPath(dest_dir_path);
    if (!status.isSuccess()) {
        status.result = "Destination path '" + dest_dir_path + "' not found";
        return status;
    }
    var dest_dir = status.result;

    var base_name = os.path.getBaseName(dest_path);
    if (source instanceof io.Directory) {
        status = this.deepCopyDir(pid, source, dest_dir, base_name, user_mode);
    }
    else {
        if (user_mode) {
            if (!this.canReadFrom(pid, source)) {
                return new StatusObject(StatusCode.FILE_IS_READ_LOCKED, "Source file '" + source_path + "' is read locked");
            }

            if (!this.canWriteTo(pid, dest_dir)) {
                return new StatusObject(StatusCode.FILE_IS_WRITE_LOCKED, "Destination '" + dest_dir_path + "' is write locked");
            }
        }

        var new_copy = new io.File(dest_dir, new io.FileSystemNodeInfo(base_name, source.info.created, source.info.modified, source.info.accessed, source.info.flags), source.data);
        status = dest_dir.setNode(new_copy, user_mode);
        if (status !== StatusCode.SUCCESS) {
            status = new StatusObject(status);
        }
        else {
            status = new StatusObject(status, new_copy);
        }
    }

    return status;
};

/**
 * Performs a deep copy of a directory, copying as many files as it can
 * @param {number} pid Process ID
 * @param {io.Directory} source
 * @param {io.Directory} parent
 * @param {string} new_name
 * @param {boolean} [user_mode] User mode will check for permissions and read locks
 * @param {boolean} [recursion] Used internally for recursion
 * @return {StatusObject}
 */
kernel.Kernel.prototype.deepCopyDir = function(pid, source, parent, new_name, user_mode, recursion) {
    if (user_mode) {
        if ((!recursion && !this.canReadFrom(pid, source)) || (recursion && !this.canReadFromSingle(pid, source)) )  {
            return new StatusObject(StatusCode.FILE_IS_READ_LOCKED, "Source directory '" + source.getPath() + "' is read locked");
        }

        if (!recursion && !this.canWriteTo(pid, parent)) {
            return new StatusObject(StatusCode.FILE_IS_WRITE_LOCKED, "Destination '" + parent.getPath() + "' is write locked");
        }
    }

    var new_copy = new io.Directory(parent, new io.FileSystemNodeInfo(new_name, source.info.created, source.info.modified, source.info.accessed, source.info.flags));
    var children = source.getChildren();
    var key, node, status;
    for (key in children) {
        if (children.hasOwnProperty(key)) {
            node = children[key];
            if (user_mode && !this.canReadFromSingle(pid, node)) {
                // Read locked, so don't copy
                continue;
            }

            if (node instanceof io.Directory) {
                status = this.deepCopyDir(pid, node, new_copy, node.info.name, user_mode, true);
                if (status.isSuccess()) {
                    // If it fails, it will just silently not copy
                    new_copy.setNode(status.result, user_mode);
                }
            }
            else {
                new_copy.setNode(new io.File(new_copy, new io.FileSystemNodeInfo(node.info.name, node.info.created, node.info.modified, node.info.accessed, node.info.flags), node.data), user_mode);
            }
        }
    }

    status = parent.setNode(new_copy, user_mode);
    if (status !== StatusCode.SUCCESS) {
        return new StatusObject(status);
    }

    return new StatusObject(StatusCode.SUCCESS, new_copy);;
};

/**
 * Attempts to move a file to a new destination
 * @param {number} pid Process ID
 * @param {string} source_path
 * @param {string} dest_path
 * @param {boolean} user_mode User mode induces permissions and lock checks
 * @return {StatusObject}
 */
kernel.Kernel.prototype.moveFile = function(pid, source_path, dest_path, user_mode) {
    var status = this._fileSystem.getNodeFromPath(source_path);
    if (!status.isSuccess()) {
        status.result = "Source file '" + source_path + "' not found";
        return status;
    }
    var source = status.result;

    var dest_dir_path = os.path.getDirName(dest_path);
    status = this._fileSystem.getNodeFromPath(dest_dir_path);
    if (!status.isSuccess()) {
        status.result = "Destination directory '" + dest_dir_path + "' not found";
        return status;
    }
    var dest_dir = status.result;

    if (user_mode) {
        if (dest_dir.hasFlag(io.FileFlag.SYSTEM)) {
            return new StatusObject(StatusCode.PERMISSION_DENIED);
        }

        var status = this.canAlterDirectory(pid, source);
        if (status !== StatusCode.SUCCESS) {
            return new StatusObject(status);
        }

        if (!this.canWriteTo(pid, dest_dir)) {
            return new StatusObject(StatusCode.FILE_IS_WRITE_LOCKED);
        }
    }

    source.parent.deleteNode(source.info.name);
    source.info.name = os.path.getBaseName(dest_path);
    dest_dir.setNode(source);
    return new StatusObject(StatusCode.SUCCESS, source);
};

module.exports = kernel;
