// LICENSE_CODE ZON ISC
'use strict'; /*jslint node:true*/
require('./config.js');
const array = require('./array.js');
const crypto = require('crypto');
const rimraf = require('rimraf');
const path = require('path');
const fs = require('fs');
const StringDecoder = require('string_decoder').StringDecoder;
const E = exports, assign = Object.assign;
// file.xxx_e() throw exceptions. file.xxx() return null/false on fail.
E.errno = 0; // an integer/string error code
// XXX sergey: please implement
E.error = null; // an Error() object
E.read_buf_size = 8192;
E.is_win = /^win/.test(process.platform);
E.is_darwin = /^darwin/.test(process.platform);

let check_file = (dst, opt)=>{
    opt = opt||{};
    if (opt.rm_rf)
        E.rm_rf(dst);
    if (opt.mkdirp)
        E.mkdirp_file_e(dst);
    if (opt.unlink)
        E.unlink(dst);
};

let FileError = (msg, code)=>{
    let err = new Error(code+' '+msg);
    err.code = code;
    return err;
};

E.read_e = (filename, opt)=>{
    if (opt===undefined)
        opt = 'utf8';
    return fs.readFileSync(filename, opt);
};
E.fread_cb_e = (fd, pos, cb)=>{
    let res, buf = Buffer.alloc(E.read_buf_size);
    while (res = fs.readSync(fd, buf, 0, buf.length, pos))
    {
        if (cb(buf.slice(0, res), pos))
            return true;
        pos += res;
    }
    return true;
};
E.read_cb_e = (filename, pos, cb)=>{
    let fd = fs.openSync(filename, 'r');
    try { return E.fread_cb_e(fd, pos, cb); }
    finally { fs.closeSync(fd); }
};
E.fread_line_cb_e = (fd, cb, opt)=>{
    opt = assign({encoding: 'utf8', buf_size: E.read_buf_size}, opt);
    cb = cb||(()=>false);
    let read, buf = Buffer.alloc(opt.buf_size);
    let strbuf = '', lf_idx, decoder = new StringDecoder(opt.encoding);
    while (read = fs.readSync(fd, buf, 0, buf.length))
    {
        strbuf += decoder.write(buf.slice(0, read));
        while ((lf_idx = strbuf.indexOf('\n'))>=0)
        {
            if (cb(strbuf.slice(0, lf_idx-(strbuf[lf_idx-1]=='\r' ? 1 : 0))))
                return true;
            strbuf = strbuf.slice(lf_idx+1);
        }
    }
    if (strbuf)
        cb(strbuf);
    return true;
};
E.read_line_cb_e = (filename, cb, opt)=>{
    let fd = fs.openSync(filename, 'r');
    try { return E.fread_line_cb_e(fd, cb, opt); }
    finally { fs.closeSync(fd); }
};
E.read_line_e = filename=>{
    let ret;
    E.read_line_cb_e(filename, line=>(ret = line, true));
    return ret;
};
E.read_lines_e = filename=>{
    let ret = E.read_e(filename).split(/\r?\n/);
    if (ret[ret.length-1]=='')
        ret.pop();
    return ret;
};
E.fread_e = (fd, start, size)=>{
    let decoder = new StringDecoder(), rest = size;
    let ret = '', append_ret = buf=>ret += decoder.write(buf);
    E.fread_cb_e(fd, start, rest===undefined ? append_ret : buf=>{
        rest -= buf.length;
        if (rest<0)
            buf = buf.slice(0, rest);
        append_ret(buf);
        return rest<=0;
    });
    return ret;
};
E.write_e = (file, data, opt)=>{
    opt = opt||{};
    check_file(file, opt);
    fs.writeFileSync(file, data, opt);
    return true;
};
E.write_atomic_e = (file, data, opt)=>{
    opt = opt||{};
    check_file(file, opt);
    let tmpfile = file+'.'+(1000000*Math.random()|0)+'.tmp';
    try {
        fs.writeFileSync(tmpfile, data, opt);
        fs.renameSync(tmpfile, file);
    } catch(e){
        E.unlink(tmpfile);
        throw e;
    }
    return true;
};
E.write_lines_e = (file, data, opt)=>{
    data = Array.isArray(data) ?
        data.length ? data.join('\n')+'\n' : '' : ''+data+'\n';
    return E.write_e(file, data, opt);
};
E.append_e = (file, data, opt)=>{
    opt = opt||{};
    check_file(file, opt);
    fs.appendFileSync(file, data, opt);
    return true;
};
E.head_e = (file, size)=>{
    if (size<0)
        size = 0;
    let fd = fs.openSync(file, 'r');
    try { return E.fread_e(fd, 0, size); }
    finally { fs.closeSync(fd); }
};
E.tail_e = (file, count)=>{
    let fd, start;
    count = count||E.read_buf_size;
    start = E.size_e(file)-count;
    if (start<0)
        start = 0;
    fd = fs.openSync(file, 'r');
    try { return E.fread_e(fd, start); }
    finally { fs.closeSync(fd); }
};
E.size_e = file=>fs.statSync(file).size;
E.mtime_e = file=>+fs.statSync(file).mtime;
function mkdirp(p, mode){
    if (typeof mode=='string')
        mode = parseInt(mode, 8);
    let made = null;
    p = path.resolve(p);
    let paths = [];
    while (p && !E.exists(p))
    {
        paths.unshift(p);
        p = path.dirname(p);
    }
    for (let i=0; i<paths.length; i++)
    {
        fs.mkdirSync(paths[i], mode);
        made = made||paths[i];
    }
    return made||p;
}
E.mkdirp_e = (p, mode)=>{
    if (mode===undefined || !process.umask)
        return mkdirp(p);
    let oldmask = process.umask(0);
    try { return mkdirp(p, mode); }
    finally { process.umask(oldmask); }
};
E.mkdirp_file_e = (file, mode)=>{
    E.mkdirp_e(path.dirname(file), mode);
    return file;
};
E.rm_rf_e = rimraf.sync;
E.unlink_e = path=>{
    fs.unlinkSync(path);
    return true;
};
E.rmdir_e = dir=>{
    fs.rmdirSync(dir);
    return true;
};
E.touch_e = path=>{
    let tm = Date.now()/1000;
    let h = fs.openSync(path, 'a');
    fs.futimesSync(h, tm, tm);
    fs.closeSync(h);
    return true;
};
E.readdir_e = (dir, opt)=>{
    let names = fs.readdirSync(dir);
    if (!opt || !!opt.dirs==!!opt.files)
        return names;
    if (opt.dirs)
        return names.filter(n=>E.is_dir(dir+'/'+n));
    return names.filter(n=>E.is_file(dir+'/'+n));
};
E.readdir_r_e = (dir, opt)=>{
    dir = dir.replace(/\/+$/, '');
    return E.find_e(dir, assign({strip: new RegExp('^'+dir+'/')}, opt));
};
function get_owner(stat, opt){
    let has_uid = 'user' in opt, has_gid = 'group' in opt;
    if (!has_uid&&!has_gid&&!opt.preserve)
        return;
    return {
        user: has_uid ? opt.user :
            opt.preserve||E.is_win ? stat.uid : process.getuid(),
        group: has_gid ? opt.group :
            opt.preserve||E.is_win ? stat.gid : process.getgid(),
    };
}
function copy_file(src, dst, opt){
    let fdw, stat, mode;
    opt = opt||{};
    stat = fs.statSync(src);
    if (E.is_dir(dst)||dst[dst.length-1]=='/')
        dst = dst+'/'+path.basename(src);
    if (opt.no_overwrite && E.exists(dst))
    {
        if (opt.no_overwrite=='skip')
        {
            if (opt.verbose)
                console.log(`Skipping copy (already exists): ${src}->${dst}`);
            return true;
        }
        throw FileError(`file already exists: ${dst}`, 'EEXIST');
    }
    check_file(dst, opt);
    mode = 'mode' in opt ? opt.mode : stat.mode & 0o777;
    fdw = fs.openSync(dst, 'w', mode);
    try {
        E.read_cb_e(src, 0, buf=>void fs.writeSync(fdw, buf, 0, buf.length));
        let owner = get_owner(stat, opt);
        if (owner)
            fs.fchownSync(fdw, owner.user, owner.group);
        // Does it really needed?
        if (opt.preserve_ts)
            fs.futimesSync(fdw, stat.atime, stat.mtime);
    } finally { fs.closeSync(fdw); }
    if (opt.verbose)
        console.log(`Copy: ${src}->${dst}`);
    return true;
}
E.copy_e = (src, dst, opt)=>{
    src = E.normalize(src);
    dst = E.normalize(dst);
    if (E.is_dir(src))
        throw FileError('cannot copy directory, use copy_r', 'EISDIR');
    return copy_file(src, dst, opt);
};
E.copy_r_e = (src, dst, opt)=>{
    opt = assign({mkdirp: true}, opt);
    src = E.normalize(src);
    dst = E.normalize(dst);
    if (opt.exclude && opt.exclude.test(src))
        return true;
    if (E.is_file(src))
        return copy_file(src, dst, opt);
    return E.readdir_r_e(src, opt).every(f=>
        E.copy_r_e(src+'/'+f, dst+'/'+f, opt));
};
E.rename_e = (src, dst)=>{
    fs.renameSync(src, dst);
    return true;
};
E.readlink_e = src=>fs.readlinkSync(src);
E.link_e = (src, dst, opt)=>{
    opt = opt||{};
    src = E.normalize(src);
    dst = E.normalize(dst);
    check_file(dst, opt);
    try { fs.linkSync(src, dst); }
    catch(e){
        if (opt.no_copy)
            throw e;
        return E.copy_r_e(src, dst, opt);
    }
    return true;
};
E.link_r_e = (src, dst, opt)=>{
    opt = assign({mkdirp: true}, opt);
    src = E.normalize(src);
    dst = E.normalize(dst);
    if (opt.exclude && opt.exclude.test(src))
        return true;
    if (!opt.follow_symlinks && E.is_symlink(src))
        return E.symlink_e(src, dst, opt);
    if (!E.is_dir(src))
        return E.link_e(src, dst, opt);
    return E.readdir_r_e(src, opt).every(f=>
        E.link_e(src+'/'+f, dst+'/'+f, opt));
};
E.symlink_e = (src, dst, opt)=>{
    opt = opt||{};
    if (E.is_win && !opt.force)
        return E.link_e(src, dst, opt);
    src = E.normalize(src);
    dst = E.normalize(dst);
    check_file(dst, opt);
    let target = src;
    if (!opt.keep_relative)
        target = fs.realpathSync(src);
    else if (E.is_symlink(src))
        target = E.readlink_e(src);
    fs.symlinkSync(target, dst);
    return true;
};
E.hashsum_e = (filename, type)=>{
    let hash = crypto.createHash(type||'md5');
    E.read_cb_e(filename, 0, buf=>void hash.update(buf));
    return hash.digest('hex');
};
let hash_re = /([0-9a-fA-F]+) [ |*](.*)/;
E.hashsum_check_e = (type, filename)=>{
    let data = E.read_lines_e(filename);
    let base = path.dirname(filename);
    for (let i=0; i<data.length; i++)
    {
        let match = hash_re.exec(data[i]);
        if (!match)
            throw new Error('Incorrect line found: '+data[i]);
        let source = E.absolutize(match[2], base);
        let hash = E.hashsum_e(source, type);
        if (hash!=match[1])
            throw new Error('Hash mismatch '+source+': '+hash+' != '+match[1]);
    }
    return true;
};

// Safe methods
function errno_wrapper(func, ret){
    let args = array.slice(arguments, 2);
    E.errno = 0;
    E.error = null;
    try { return func.apply(null, args); }
    catch(e){
        E.errno = e.code||e;
        E.error = e;
        return ret;
    }
}
function find_cb(dir, cb, opt){
    opt = opt||{};
    let exclude = opt.exclude, match = opt.match, strip = opt.strip;
    function proc(f){
        if (match && !match.test(f))
            return;
        cb(f);
    }
    E.readdir_e(dir).forEach(f=>{
        let name = E.normalize(dir+'/'+f);
        let stripped = strip ? name.replace(strip, '') : name;
        if (exclude && exclude.test(stripped))
            return;
        if (E.is_dir(name))
        {
            if (opt.dirs)
                proc(stripped);
            if (!opt.follow_symlinks && E.is_symlink(name))
                return;
            find_cb(name, cb, opt);
        }
        else
            proc(stripped);
    });
}
E.find_e = (dir, opt)=>{
    opt = opt||{};
    if (opt.cb)
        return find_cb(dir, opt.cb, opt);
    let ret = [];
    find_cb(dir, f=>ret.push(f), opt);
    return ret;
};
E.realpath_e = path=>fs.realpathSync(path);
E.stat_e = path=>fs.statSync(path);
E.lstat_e = path=>fs.lstatSync(path);
let err_retval = {
    read: null, read_line: null, read_lines: null, fread: null,
    tail: null, head: null, size: null, mkdirp: null, mkdirp_file: null,
    hashsum: null, stat: null, lstat: null, realpath: null, readlink: null,
    hashsum_check: false, fread_cb: false, read_cb: false,
    fread_line_cb: false, read_line_cb: false, write: false,
    write_atomic: false, write_lines: false,
    append: false, unlink: false, rmdir: false, rm_rf: false, touch: false,
    readdir: [], readdir_r: [], copy: false, copy_r: false, rename: false,
    link: false, link_r: false, symlink: false, mtime: -1, find: null,
};
for (let method in err_retval)
    E[method] = errno_wrapper.bind(null, E[method+'_e'], err_retval[method]);

E.exists = path=>{
    try { fs.accessSync(path); }
    catch(e){ return false; }
    return true;
};
E.is_file = path=>{
    let stat;
    try { stat = fs.statSync(path); }
    catch(e){ return false; }
    return stat.isFile();
};
E.is_dir = path=>{
    let stat;
    try { stat = fs.statSync(path); }
    catch(e){ return false; }
    return stat.isDirectory();
};
E.is_symlink = path=>{
    let stat;
    try { stat = fs.lstatSync(path); }
    catch(e){ return false; }
    return stat.isSymbolicLink();
};
E.is_chardev = path=>{
    let stat;
    try { stat = fs.statSync(path); }
    catch(e){ return false; }
    return stat.isCharacterDevice();
};
E.is_socket = path=>{
    let stat;
    try { stat = fs.statSync(path); }
    catch(e){ return false; }
    return stat.isSocket();
};
E.is_exec = path=>{
    try { fs.accessSync(path, fs.X_OK); }
    catch(e){ return false; }
    return true;
};
E.which = bin=>{
    bin = E.normalize(bin);
    if (E.is_absolute(bin)&&E.is_exec(bin))
        return bin;
    let paths = process.env.PATH.split(E.is_win ? ';' : ':');
    for (let i=0; i<paths.length; i++)
    {
        let filename = E.normalize(`${paths[i]}/${bin}`);
        // In cygwin .exe extensions is omitting
        if (E.is_win&&!E.exists(filename)&&E.exists(filename+'.exe'))
            filename += '.exe';
        if (E.exists(filename)&&E.is_exec(filename))
            return filename;
    }
};
let watch_files = {};
E.file_changed = (file_path, scope)=>{
    scope = scope||'';
    let mtime = E.mtime(file_path);
    watch_files[scope] = watch_files[scope]||{};
    if (watch_files[scope][file_path]===mtime)
        return false;
    watch_files[scope][file_path] = mtime;
    return true;
};

if (E.is_win)
{
    E.cygwin_root = E.is_dir('C:/cygwin') ? 'C:/cygwin' :
        E.is_dir('D:/cygwin') ? 'D:/cygwin' : null;
}
E.cyg2unix = path=>{
    if (!E.is_win || !E.cygwin_root)
        return path;
    // /cygdrive/X/yyy --> X:/yyy
    path = path.replace(/^\/cygdrive\/(.)(\/(.*))?$/, '$1:/$3');
    // /usr/lib --> c:/cygwin/lib
    path = path.replace(/^\/usr\/lib(\/.*)?$/, E.cygwin_root.toLowerCase()+
        '/lib$1');
    // /usr/bin --> c:/cygwin/bin
    path = path.replace(/^\/usr\/bin(\/.*)?$/, E.cygwin_root.toLowerCase()+
        '/bin$1');
    // /xxx --> c:/cygwin/xxx
    path = path.replace(/^\//, E.cygwin_root.toLowerCase()+'/');
    return path;
};
E.unix2cyg = path=>{
    if (!E.is_win)
        return path;
    // c:/cygwin/lib -> /usr/lib
    path = path.replace(new RegExp('^'+E.cygwin_root.toLowerCase()+'/lib(.*)'),
        '/usr/lib$1');
    // c:/cygwin/bin -> /usr/bin
    path = path.replace(new RegExp('^'+E.cygwin_root.toLowerCase()+'/bin(.*)'),
        '/usr/bin$1');
    // c:/cygwin/xxx -> /xxx
    path = path.replace(new RegExp('^'+E.cygwin_root.toLowerCase()+'/(.*)'),
        '/$1');
    return path;
};
E.unix2win = path=>{
    if (!E.is_win)
        return path;
    // c:/xxx -> C:/xxx
    path = path.replace(/^[c-z]:/, s=>s.toUpperCase());
    // C:/xxx/yyy -> C:\xxx\yyy
    path = path.replace(/\//g, '\\');
    return path;
};
E.win2unix = (path, force)=>{
    if (!force && !E.is_win)
        return path;
    // C:\xxx\yyy --> C:/xxx/yyy
    path = path.replace(/\\/g, '/');
    // C:/ --> c:/
    path = path.replace(/^[c-z]:/i, s=>s.toLowerCase());
    return path;
};
E.win2cyg = path=>{
    if (!E.is_win)
        return path;
    path = E.win2unix(path);
    let escaped_root = E.cygwin_root.replace(/([\?\\\/\[\]+*])/g, '\\$1');
    path = path.replace(new RegExp('^'+escaped_root+'/?', 'i'), '/');
    path = path.replace(/^[c-z]:/i, s=>'/cygdrive/'+s[0].toLowerCase());
    return path;
};
E.is_absolute = path=>/^(\/|([c-z]:))/i.test(path);
E.absolutize = (p, d1, d2)=>{
    if (!p||E.is_absolute(p))
        return p;
    if (d2&&E.exists(d2+'/'+p))
        return d2+'/'+p;
    return d1+'/'+p;
};
E.normalize = p=>E.cyg2unix(E.win2unix(path.normalize(p)));
E.is_subdir = (root, sub)=>{
    let nroot = root.length;
    return !root || sub.startsWith(root) && (root[nroot-1]=='/' ||
        sub[nroot]===undefined || sub[nroot]=='/');
};
