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

const { join } = require('path');

let rootPath = null;

class FSWrapper {
    /**
     * Set root path which will be used to create full path.
     * @param {String} _rootPath 
     */
    static setRootPath(_rootPath) {
        rootPath = _path.normalize(_rootPath);
    }

    static getRootPath() {
        return rootPath;
    }

    /**
     * Return platform based separator for path.
     * @returns {String} platform based path separator.
     */
    static getPlatformSeparator() {
        return _path.sep;
    }

    static calculateRootPath(pattern) {
        let rootPath = pattern;
        if (pattern instanceof RegExp) {
            const splitedPath = __dirname.split(_path.sep);

            while (splitedPath.length > 0) {
                const segment = splitedPath.pop();
                if (segment.match(pattern)) {
                    splitedPath.push(segment);
                    rootPath = splitedPath.join(_path.sep);
                    break;
                }
            }
        }

        FSWrapper.setRootPath(rootPath);

        return rootPath;
    }

    /**
     * Reads JSON file.
     * @param {String} path relative part from rootPath
     * @param {String} base default rootPath or part to root.
     * @returns {Object}
     */
    static readJSONFileSync(path, base = rootPath) {
        return JSON.parse(fs.readFileSync(
            FSWrapper.normalizeValidatedPath(path, base)
        )
        );
    }

    /**
     * Write JSON file as new file.
     * @param {*} content JSON
     * @param {String} path relative part from rootPath
     * @param {String} base default rootPath or part to root.
     */
    static writeJSONFileSync(content, path, base = rootPath) {
        fs.writeFileSync(
            FSWrapper.normalizeValidatedPath(path, base),
            new Uint8Array(Buffer.from(JSON.stringify(content, undefined, 4)))
        );
    }

    /**
     * Returns true if the path exists, false otherwise.
     * @param {String} path relative part from rootPath
     * @param {String} base default rootPath or part to root.
     * @returns {boolean}
     */
    static isPathExistSync(path, base = rootPath) {
        return fs.existsSync(
            FSWrapper.normalizeValidatedPath(path, base)
        );
    }

    /**
     * Recursive copy of folder.
     * @param {String} contentsDirectory 
     * @param {String} destinationFolder 
     * @param {String} contentBasePath 
     * @param {String} destinationBasePath 
     * @param {String[]} ignoreList list of files/folders to be ignored on copy check last segment of path.
     */
    static copyFolderContentsRecursiveSync(contentsDirectory, destinationFolder, contentBasePath, destinationBasePath, ignoreList = []) {
        if (contentsDirectory.includes('.git')
            || !FSWrapper.isPathExistSync(contentsDirectory, contentBasePath)) {
            return;
        }

        const isContentPathDirectory = FSWrapper.lstatSync(contentsDirectory, contentBasePath).isDirectory();
        // Create destination folder
        if (isContentPathDirectory && !FSWrapper.isPathExistSync(destinationFolder, destinationBasePath)) {
            FSWrapper.createDirectorySync(destinationFolder, destinationBasePath);
        }

        if (isContentPathDirectory) {
            FSWrapper.readDirSync(contentsDirectory, contentBasePath)
                .forEach((fileOrDirectory) => {
                    const currentPath = join(contentsDirectory, fileOrDirectory);
                    const newPath = join(destinationFolder, fileOrDirectory);
                    if (FSWrapper.isIgnored(currentPath, ignoreList)) {
                        return;
                    }

                    if (FSWrapper.lstatSync(currentPath, contentBasePath).isDirectory()) {
                        FSWrapper.createDirectorySync(newPath, destinationBasePath);
                        FSWrapper.copyFolderContentsRecursiveSync(currentPath, newPath, contentBasePath, destinationBasePath, ignoreList);
                    } else {
                        FSWrapper.copyFileSync(currentPath, newPath, contentBasePath, destinationBasePath);
                    }
                });
        } else {
            if (FSWrapper.isIgnored(contentsDirectory, ignoreList)) {
                return;
            }
            FSWrapper.copyFileSync(contentsDirectory, destinationFolder, contentBasePath, destinationBasePath);
        }
    }

    /**
     * Synchronously creates a directory. Creates all missing directories in path.
     * @param {String} dirPath 
     */
    static createDirectorySync(dirPath, dirBasePath = rootPath) {
        dirPath = _path.normalize(dirPath);
        dirBasePath = _path.normalize(this.validateBasePath(dirBasePath));

        let pathSegments = _path.sep;
        const splittedPath = dirPath.split(_path.sep);

        // Some issue with joining Disk letter, slash missing.
        const isDrive = splittedPath[0].match('[a-zA-Z]{1}:');
        if (isDrive) {
            pathSegments = `${splittedPath.shift()}${_path.sep}`;
        }

        splittedPath.forEach((pathSegment) => {
            pathSegments = join(pathSegments, pathSegment);

            if (!FSWrapper.isPathExistSync(pathSegments, dirBasePath)) {
                FSWrapper.mkDirSync(pathSegments, dirBasePath);
            }
        });
    }

    /**
     * Creates directory.
     * @param {String} path relative part from rootPath
     * @param {String} base default rootPath or part to root.
     */
    static mkDirSync(path, base = rootPath) {
        fs.mkdirSync(
            FSWrapper.normalizeValidatedPath(path, base),
            (err) => {
                if (err) {
                    return console.error('mkDirSync', err);
                }
            });
    }

    /**
     * Wrapper for fs.lstat function.
     * @param {String} path 
     * @param {String?} base 
     * @returns {Object} fs.lstat result object.
     */
    static lstatSync(path, base = rootPath) {
        return fs.lstatSync(FSWrapper.normalizeValidatedPath(path, base));
    }

    /**
     * Reads the contents of the directory
     * @param {String} path relative part from rootPath
     * @param {String} base default rootPath or part to root.
     * @returns 
     */
    static readDirSync(path, base = rootPath) {
        return fs.readdirSync(FSWrapper.normalizeValidatedPath(path, base));
    }

    /**
     * Synchronously copies src to dest.
     * @param {String} from relative part from rootPath
     * @param {String} to relative part from rootPath
     * @param {String} baseFrom default rootPath or part to root.
     * @param {String} baseTo default rootPath or part to root.
     */
    static copyFileSync(from, to, baseFrom = rootPath, baseTo = rootPath) {
        const srcPath = FSWrapper.normalizeValidatedPath(from, baseFrom);
        const targetPath = FSWrapper.normalizeValidatedPath(to, baseTo);

        const splittedTo = to.split(FSWrapper.getPlatformSeparator());
        const destinationFolder = FSWrapper.combinePath(splittedTo.slice(0, splittedTo.length - 1));
        if (!FSWrapper.isPathExistSync(destinationFolder, baseTo)) {
            FSWrapper.createDirectorySync(destinationFolder, baseTo);
        }

        fs.copyFileSync(
            srcPath,
            targetPath
        );
    }

    /**
     * Synchronous rmdir(2). Returns undefined.
     * @param {String} path relative part from rootPath.
     * @param {String} base default rootPath or part to root.
     */
    static rmDirSync(path, base = rootPath) {
        if (!FSWrapper.isPathExistSync(path, base)) {
            return;
        }

        FSWrapper.readDirSync(path, base).forEach((entry) => {
            const entry_path = _path.normalize(join(path, entry));
            if (FSWrapper.lstatSync(entry_path, base).isDirectory()) {
                FSWrapper.rmDirSync(entry_path, base);
            } else {
                fs.unlinkSync(FSWrapper.normalizeValidatedPath(entry_path, base));
            }
        });
        fs.rmdirSync(FSWrapper.normalizeValidatedPath(path, base));
    }

    static validateBasePath(basePath) {
        return basePath || '';
    }

    /**
     * Join all segments in full path.
     * @param {String[]} pathList 
     * @returns {String} normalized combined path
     */
    static combinePath(pathList) {
        return _path.normalize(_path.join(...pathList));
    }

    /**
     * 
     * @param {String} path path to search.
     * @param {RegExp} pattern search pattern.
     * @param {String[]?} skipName list of names to skip.
     * @param {Boolean?} isSearchOnMatch work as find, will stop recursion.
     * @param {Function?} onMatch handle function called when regex match path.
     * @returns {String[]} list of matched pathes.
     */
    static recursiveSearch(path, pattern, skipName = [], isSearchOnMatch = true, onMatch) {
        let result = [];
        if (!FSWrapper.isPathExistSync(path, '')) {
            return false;
        }

        FSWrapper.readDirSync(path, '').forEach((fileName) => {
            if (skipName.includes(fileName)) {
                return;
            }

            const innerPath = FSWrapper.combinePath([path, fileName]);
            const pathStat = FSWrapper.lstatSync(innerPath, '');

            const isMatch = Boolean(fileName.match(pattern));
            if (isMatch) {
                result.push(innerPath);
            }

            if (pathStat.isDirectory() && ((isMatch && isSearchOnMatch) || !isMatch)) {
                result = result.concat(FSWrapper.recursiveSearch(innerPath, pattern, skipName, isSearchOnMatch, onMatch));
            }

            if (onMatch && isMatch) {
                onMatch(innerPath, pathStat);
            }

        });

        return result;
    }

    static isIgnored(path, ignoreList) {
        const expression = new RegExp(ignoreList.map((ignoreMember) => `[\\\\/]{1,2}${ignoreMember.replace('.', '\\.')}$`).join('|'));

        return ignoreList.length > 0 && expression.test(_path.normalize(path));
    }

    static normalizeValidatedPath(path, base) {
        return _path.normalize(join(FSWrapper.validateBasePath(base), path));
    }

    /**
     * fs.readFileSync wrapper.
     * @param {String} path
     * @param {String} base
     * @param {*String | Object} options
     * @returns {String | Buffer}
     */
    static readFileSync(path, base = rootPath, options) {
        return fs.readFileSync(FSWrapper.normalizeValidatedPath(path, base), options);
    }

    /**
     * For files will call `rmFileSync` for folders 'rmDirSync'.
     * @param {String} path
     * @param {String} base
     */
    static rmContentSync(path, base = rootPath) {
        if (FSWrapper.lstatSync(path, base).isDirectory()) {
            FSWrapper.rmDirSync(path, base);
        } else {
            FSWrapper.rmFileSync(path, base);
        }
    }

    /**
     * Wrapper for fs.unlinkSync. Used to delete files.
     * @param {String} path
     * @param {String} base
     */
    static rmFileSync(path, base = rootPath) {
        fs.unlinkSync(FSWrapper.normalizeValidatedPath(path, base));
    }
}

module.exports = FSWrapper;
