"use strict";

var fs           = require('fs');
var Path         = require('path');
var mime         = require('mime');
var crypto       = require('crypto');
var zlib         = require('zlib');
var EventEmitter = require('events').EventEmitter;
var console      = require('gifer-console');
var URL          = require('url');
var Readable     = require('stream').Readable;

// var limitSizeFileToCache = 15728640;

function formatFileSize(size){
    var index = 0;

    while (size > 1024){
        index ++;

        size = size / 1024;
    }

    return Math.round(size * 1000)/1000 + " " + ["B","KB","MB","GB","TB","PB","EB"][index];
}

function FileCache(defaultOptions) {
    var cacheHandler = new EventEmitter();
    var pendingList  = [];
    var inProgress   = false;
    var cacheInProgressList = {};

    // ============================================================================

    var cache = new function CacheContainer(){
        var fullPathList  = new Map();
        var content       = new Map();
        var watchers      = new Map();
        var Self          = this;
        var reportTimer   = null;
        var contentLength = 0;
        var contentSize   = 0;

        function startTTL(){
            setInterval(function (){
                var removeList = [];
                var cnt = getContent();

                Object.keys(cnt).forEach(function (path){
                    cnt[path].TTL --;

                    if (cnt[path].TTL <= 0) {
                        removeList.push(cnt[path].fullPath); // p - full path
                    }
                });

                if (removeList.length) {
                    removeList.forEach(function (path){
                        removeFromCache(path);

                        // console.log("remove from cache path: " + path);
                    });
                }

                removeList = null;
            }, 1000 * 60 * 60); //
        }

        function calcSize(clc){
            var size = clc.length || 0;

            if (clc.gzip){
                size += clc.gzip.length || 0;
            } else
            if (clc.deflate){
                size += clc.deflate.length || 0;
            }

            return size;
        }

        function getContent(){
            return content;
        }

        function calcHash(data){
            return crypto.createHash('sha256').update(data).digest('hex');
        }

        function createReport(){
            if (!reportTimer){
                reportTimer = setTimeout(function(){
                    reportTimer = null;

                    console.info("Cache size: " + formatFileSize(Self.getTotalCacheSize()) + " " + "Cache list: " + contentLength, console.logLevel.INFO);
                }, 1000);
            }
        }

        function removeFromCache(_fullPath){
            var fullPathHash = calcHash(_fullPath);

            if (fullPathList.has(fullPathHash)){
                var pathHash = fullPathList.get(fullPathHash);//crc.crc32(_path).toString();

                contentLength --;
                contentSize   -= calcSize(content.get(pathHash));

                try {
                    watchers.get(fullPathHash).close();
                } catch(e){
                    console.warn("removeFromCache: " + e.message);
                }

                watchers    .delete(fullPathHash);
                fullPathList.delete(fullPathHash)
                content     .delete(pathHash);

                createReport();

                console.warn('Removing from cache: ' + _fullPath + " count: " + contentLength, console.logLevel.DEBUG);
            }
        }

        this.addToCache = function(data, _watcher, options){
            if (data.urlPath[0] === "/"){
                data.urlPath = data.urlPath.replace("/", "");
            }

            var pathHash     = calcHash(data.urlPath);//
            var fullPathHash = calcHash(data.fullPath);//

            if (content.has(pathHash)){
                console.info('The file was updated: ' + content.get(pathHash).fullPath, console.logLevel.INFO);

                // contentSize -= content[pathHash].length || 0;
                contentSize -= calcSize(content.get(pathHash));
            } else {
                contentLength ++;

                data.TTL = MCServer.WEBCacheTTL;
            }

            watchers    .set(fullPathHash, _watcher);
            content     .set(pathHash, data);
            fullPathList.set(fullPathHash, pathHash);

            contentSize += calcSize(data);

            createReport();
        };

        this.findFileInCache = function(_path){
            var pathHash = calcHash(_path);//crc.crc32(_path).toString();
            var cntData  = content.get(pathHash);

            if (cntData){
                cntData.TTL = MCServer.WEBCacheTTL;
            }

            return cntData;
        };

        this.findPathInCache = function(_path){
            var pathHash = calcHash(_path);//crc.crc32(_path).toString();
            var cntData  = fullPathList.get(pathHash);// || null;

            return cntData;
        };

        this.removeFromCache = removeFromCache;

        this.clearCache = function(){
            // Object.keys(watchers).forEach(function (path){
            //     try {
            //         watchers[path].close();
            //     } catch(e){}
            // });
            for (let watcher of watchers.values()){
                try {
                    watcher.close();
                } catch (e) {}
            }

            content     .clear();
            fullPathList.clear();
            watchers    .clear();
            
            contentLength = 0;
            contentSize   = 0;

            createReport();
        };

        this.getTotalCacheSize = function(){
            return contentSize;
        };

        this.contentLength = function(){
            return contentLength;
        };

        startTTL();
    };

    // ============================================================================

    var httpHandler = function (options, cache, cacheHandler) {
        options = options || {
            etag    : true,
            lastmod : true,
            expires : 3600000, // milliseconds,
            maxAge  : 3600     // seconds
        };

        function loadFile(res, req, next, contents){
            var headers    = {};

            // sets the content to serve
            headers['Content-Type'] = contents.mime_type + '; charset=UTF-8';
            headers['CacheFile'] = 'mychat cache system';

            // http cache
            if (options) {
                if (options.etag) {
                    if (req.headers['if-none-match'] != null && req.headers['if-none-match'] === contents.hash) {
                        res.writeHead(304);
                        res.end();

                        return next && next(false);
                    }

                    headers['ETag'] = contents.hash;
                }

                if (options.lastmod) {
                    if (req.headers['if-modified-since'] != null && new Date(req.headers['if-modified-since']).getTime() === contents.mtime.getTime()) {
                        res.writeHead(304);
                        res.end();

                        return next && next(false);
                    }

                    headers['Last-Modified'] = contents.mtime.toUTCString();
                }

                if (options.expires) {
                    headers['Expires'] = new Date(contents.mtime + (options.expires*1000)).toUTCString();
                }

                if (options.maxAge) {
                    headers['Max-Age'] = options.maxAge;
                }

                headers['Cache-Control'] = 'public';
            }

            let cnt = contents;

            if (contents.gzip && req.headers['accept-encoding'] && req.headers['accept-encoding'].indexOf('gzip') !== -1) {
                headers['Content-Encoding'] = 'gzip';
                cnt = contents.gzip;
            }

            // sends the contents
            headers['Content-Length'] = cnt.length + "";

            res.writeHead(200, headers);

            var SIZE   = 65534;
            var offset = 0;

            // console.log("loadFile: " + req.url);

            var goRead = function (cnt, _res){
                if (offset < cnt.length && offset + SIZE > cnt.length){
                    SIZE = cnt.length - offset;
                }

                if (offset === cnt.length){
                    // console.log("loadFile finish1: " + req.url);

                    _res.end();
                } else {
                    var buf = Buffer.alloc(SIZE);
                    cnt.copy(buf, 0, offset, offset + SIZE);

                    if (_res.writable) {
                        try {
                            _res.write(buf, "binary");

                            if (SIZE === cnt.length || (offset + SIZE) === cnt.length) {
                                // console.log("loadFile finish2: " + req.url);

                                _res.end(null, "binary");
                            }
                        } catch (e){
                            console.warn(e.message);
                        }
                    }
                }
            };

            goRead(cnt, res);

            res.on('drain', function () {
                offset = offset + SIZE;

                goRead(cnt, res);
            });
        }

        function handleRequest(uri, root, req, res, next) {
            var url      = URL.parse(uri);
            var filePath = (root + url.pathname).replace("//", "/");
            var pathname = url.pathname || url.path;

            req.urlparts = url;

            if (pathname === null) {
                pathname = "/index.html";
            } else

            if (pathname[pathname.length - 1] === '/') {
                pathname += 'index.html';
            }

            // console.log("handleRequest: " + url.pathname);

            var fileCacheData = cache.findFileInCache(pathname[0] === "/" ? pathname.replace("/", "") : pathname);

            if (!fileCacheData) {
                if (cacheHandler.canCache(filePath)){
                    options.root = root;

                    if (next) {
                        next(true);

                        // console.log("handleRequest from disk: " + url.pathname);

                        cacheHandler.addToCacheWithTTL(options, filePath, pathname, root, function(err){
                           if (err) console.warn("handleRequest path: " + filePath + "\nError: " + err);
                        });
                    } else {
                        res.statusCode = 404;
                        res.end();
                    }
                } else

                // if (options.handler) {
                //     options.handler(req, res, next);
                // } else

                if (next) {
                    next(true);
                } else {
                    res.statusCode = 404;
                    res.end();
                }
            } else {
                res.statusCode = 200;

                loadFile(res, req, next, fileCacheData);
            }
        }

        return handleRequest;
    };

    // ============================================================================

    function doArchive(data, options, callback) {
        if (options.gzip) {
            try {
                zlib.gzip(data, function(err, res) {
                    if (err) {
                        callback(err, null);
                    } else {
                        callback(null, res);
                    }
                });
            } catch(e){
                callback(null, data);
            }
        } else {
            callback(null, data);
        }
    }

    function watchHandle(path, options, em){
        var watcher = null;

        try {
            watcher = fs.watch(path);

            watcher.on('change', function() { // event, filename
                try {
                    watcher.close();
                } catch(e){}

                if (cache && cache.findPathInCache(path)) {
                    generate(path, options, em);
                }
            });

            watcher.on('error', function(err) {
                try {
                    watcher.close();
                } catch(e){}

                cache.removeFromCache(path);

                console.err("watchHandle: " + JSON.stringify(err), console.logLevel.ERROR);

                em.emit('error', err);
            });
        } catch(e){
            cache.removeFromCache(path);

            watcher = null;

            console.err("try watchHandle: " + e.message, console.logLevel.ERROR);

            em.emit('error', e.message);
        }

        return watcher;
    }

    function ExtractFileExtension(Path){
        var res = Path.split('.');

        res = res.length > 1 ? res[res.length - 1] : "";

        return res;
    }

    function ExtractFileName(Path){
        var count = 0;

        for (var i = Path.length - 1; i >= 0; i--){
            count ++;

            if ((Path[i] === '\\') || (Path[i] === '/')) {
                return Path.slice(i + 1, i + count);
            }
        }

        return Path;
    }

    function generate(path, options, em, eventName) {
        // var start = path;
        var queue = [path];

        function prep(newData, fileInfo, path, options, em){
            newData.fullPath  = path;
            newData.urlPath   = (options.prefix || "") + (options.root ? path.replace(options.root, "") : path).replace(/\\/g, '/');
            newData.mtime     = fileInfo.mtime;
            newData.mime_type = mime.lookup(path);

            if (options.hashAlgo) {
                var hash = crypto.createHash(options.hashAlgo);
                hash.update(newData);

                newData.hash = hash.digest('hex');
            }

            if (options.watchFileChange) {
                var watcher = watchHandle(path, options, em);
            }

            if (watcher) {
                cache.addToCache(newData, watcher, options);
            } else 
            if (options.watchFileChange) {
                console.warn("Cache watcher not created for: " + path);
            }

            read();
        }

        function readFile(path, fileInfo){
            fs.readFile(path, function(err, fileData) {
                if (err) {
                    cache.removeFromCache(path);

                    em.emit('error', err);
                } else {
                    let doArc   = ! (["png", "jpeg", "jpg", "7z", "zip", "rar", "docx", "xlsx", "gz", "apk"].indexOf(ExtractFileExtension(path)) >= 0) ;
                    let newData = fileData;

                    if (doArc){
                        doArchive(fileData, options, function(err, data) {
                            if (err) {
                                em.emit('error', err);
                            } else {

                                if (data.length < newData.length){
                                    // newData = data;
                                    newData.gzip = data;
                                }

                                options.root = options.root || Path.dirname(path);

                                prep(newData, fileInfo, path, options, em);
                            }
                        })
                    } else {
                        options.root = options.root || Path.dirname(path);

                        prep(newData, fileInfo, path, options, em);
                    }
                }
            });
        }

        function getFileInfo(path){
            fs.stat(path, function(err, fileInfo) {
                if (err) {
                    cache.removeFromCache(path);

                    em.emit('error', err);
                } else {
                    if (fileInfo.isDirectory()) {
                        fs.readdir(path, function(err, files) {
                            if (err) {
                                em.emit('error', err);
                            } else {
                                options.root = options.root || path;

                                // if (options.watchDirectoryChange) {
                                //watchHandle(path, options, em);
                                // }

                                queue = queue.concat(files.map(function(file) {
                                    return Path.resolve(path, file)
                                }));

                                read();
                            }
                        })
                    } else
                    if (cache.getTotalCacheSize() >= MCServer.WEBCacheTotalSizeLimit) {
                        // ignore cache, clear queue
                        queue = [];
                    } else
                    if (!canCache(path)) {
                        em.emit('errorLimit', {
                            path : path,
                            size : fileInfo.size,
                            limit: MCServer.WEBCacheOneFileSizeLimit
                        });

                        cache.removeFromCache(path);
                    } else {
                        readFile(path, fileInfo);
                    }
                }
            })
        }

        function read() {
            if (queue.length === 0) {
                em.emit(eventName || 'done', cache /*path*/);
            } else {
                var path = queue.shift();

                getFileInfo(path);
            }
        }

        read();
    }

    function genOptions(options, defaults) {
        Object.keys(defaults).forEach(function(k) {
            if(!options.hasOwnProperty(k)) {
                options[k] = defaults[k]
            }
        });

        return options;
    }

    function canCache(path){
        var res = false;

        try {
            var stat = fs.statSync(path);

            path = path.replace(/\\/g, "/");

            if (stat.isFile() && (stat.size < MCServer.WEBCacheOneFileSizeLimit || defaultOptions.mandatory.indexOf(path) >=0) ) {
                res = true;
            }/* else {
                console.log(path);
            }*/
        } catch (e){
            console.err("fileCache error: " + e.message, console.logLevel.ERROR);
        }

        return res;
    }

    // ============================================================================

    cacheHandler.canCache = canCache;

    cacheHandler.addToCacheWithTTL = function addToCacheWithTTL (options, path, uriPath, root, callBack){
        var startCache = false;
        var isFile = ExtractFileName(path) !== "";

        if (uriPath[0] === "/"){
            uriPath = uriPath.replace("/", "");
        }

        if (!isFile){
            callBack && callBack("Can't add folder to the cache with, please add only one file");
        } else
        if (!cacheInProgressList[uriPath]){
            var cFile = cache.findFileInCache(uriPath);

            if (cFile) {
                callBack && callBack(null, cFile);
            } else {
                cacheInProgressList[uriPath] = [callBack];

                startCache = true;
            }
        } else
        if (callBack) {
            cacheInProgressList[uriPath].push(callBack);
        }

        if (startCache){
            var ev  = new EventEmitter();
            var opt = Object.assign({}, options);
                opt.root = root;

            // console.log("addToCacheWithTTL: " + uriPath);

            generate(path, opt, ev, path);

            ev.once(path, function (){
                // var cFile = cache.findFileInCache(uriPath);
                //
                // if (!cFile){
                //     // console.log(uriPath);
                //
                //     cFile = cache.findFileInCache(uriPath);
                // } else {
                //     console.log("addToCacheWithTTL add file: " + uriPath, console.logLevel.L_Full);
                // }
                //
                // if (cacheInProgressList[uriPath]) {
                //     while (cacheInProgressList[uriPath].length){
                //         cacheInProgressList[uriPath].shift()(null, cFile);
                //     }
                // }
                
                try {
                    delete cacheInProgressList[uriPath];
                } catch (e) {}
            });

            ev.once("errorLimit", function (){
                try {
                    delete cacheInProgressList[uriPath];
                } catch (e) {}

                callBack && callBack("errorLimit: can cache");
            });

            ev.once("error", function (err){
                try {
                    delete cacheInProgressList[uriPath];
                } catch (e) {}
                
                callBack && callBack("error: can cache");
                
                console.err("error addToCacheWithTTL: " + err.message, console.logLevel.ERROR);
            });
        }
    };

    cacheHandler.options = function(options) {
        Object.keys(options).forEach(function(k) {
            defaultOptions[k] = options[k];
        })
    };

    cacheHandler.load = function(path, options, cb) {
        function onDone() {
            cacheHandler.removeListener('done', onDone);

            if (pendingList.length === 0) {
                cb && cb(null, cache);

                cacheHandler.emit('ready', cache); // {path: start, totalSize:  cache.getTotalCacheSize()}

                inProgress = false;
            } else {
                generate.apply(null, pendingList.shift())//(path, options, eventHandler);

                cacheHandler.on('done', onDone);
            }
        }

        if (typeof options === 'function') {
            cb      = options;
            options = {};
        }

        options = genOptions(options || {}, defaultOptions);
        path    = Path.resolve(path);

        if (inProgress){
            pendingList.push([ path, options, cacheHandler ]);
        } else {
            inProgress = true;

            generate(path, options, cacheHandler);

            cacheHandler.on('done', onDone);
        }

        return cacheHandler;
    };

    cacheHandler.httpHandler = function(options) {
        options = genOptions(options || {}, defaultOptions);

        return httpHandler(options, cache, cacheHandler);
    };

    cacheHandler.clearCache  = function() {
        cache.clearCache();
    };

    cacheHandler.removeFromCache = function(path){
        cache.removeFromCache(path);
    };

    // em.setMaxListeners(20);

    // if (dir) {
    //     cacheHandler.load(dir, fileCacheCB)
    // }

    return cacheHandler;
}

module.exports = FileCache;

