const fs = require('fs');
const path = require('path');
const childProcess = require('child_process');
const defaultsDeep = require('lodash.defaultsdeep');
const promiseify = require('./promiseify');
const access = promiseify(fs.access);

const WORKER_ENV = 'WORKER_ENV';

const DATA_TAG = 'WORKER_JSON_MESSAGE';
const DATA_TAG_OPEN = `<${DATA_TAG}>`;
const DATA_TAG_CLOSE = `</${DATA_TAG}>`;
const DATA_TAG_REG = new RegExp(`${DATA_TAG_OPEN}(.*?)${DATA_TAG_CLOSE}`);


process.env[WORKER_ENV] = WORKER_ENV;


const decode = content => {
    let match = String(content).match(DATA_TAG_REG);
    if (match) {
        return JSON.parse(match[1]);
    }
};


const encode = data => {
    let json = JSON.stringify(data);
    json = `${DATA_TAG_OPEN}${json}${DATA_TAG_CLOSE}`;
    return json;
};


/**
 * 使用子进程运行命令
 * @see https://nodejs.org/api/child_process.html#child_process_child_process_exec_command_options_callback
 * @param   {string}    command     命令
 * @param   {Object}    options     进程选项
 * @return  {Promise}
 */
const worker = (command, options = {}) => {

    return new Promise((resolve, reject) => {

        // 子进程继承父进程的环境变量
        options = defaultsDeep({}, options, { env: process.env});

        // 将 node_modules/.bin 加入环境变量，优先使用本地模块执行命令
        let addPath = cwd => {
            let env = options.env;
            if (cwd && env) {
                let localBin = path.resolve(cwd, 'node_modules', '.bin');
                if (env.PATH && !env.PATH.includes(localBin)) {
                    return access(localBin).then(() => {
                        env.PATH = localBin + ':' + env.PATH;
                    }).catch(() => {});
                }
            }
        };


        Promise.all([
            addPath(process.cwd()),
            addPath(options.cwd)
        ]).then(() => {
            let child;
            let timer = null;

            let jsonMode = false;
            let buffers = [];
            let nread = 0;

            let callback = (errors, data) => {
                callback = () => { };
                clearTimeout(timer);

                if (errors) {
                    console.error(`> ${command}`);
                    reject(errors);
                } else {
                    resolve(data);
                }

                if (child) {
                    child.kill();
                }
            };


            try {
                child = childProcess.exec(command, options)
            } catch (errors) {
                callback(errors);
            }


            child.stdout.on('data', content => {

                if (!jsonMode) {
                    content = content.toString();
                    jsonMode = content.includes(DATA_TAG_OPEN);
                }

                if (jsonMode) {
                    buffers.push(content);
                    nread += content.length;
                } else {
                    process.stdout.write(content);
                }
            });


            child.stdout.on('end', () => {
                if (!jsonMode) {
                    return;
                }

                let buffer = null;

                switch (buffers.length) {
                    case 0:
                        buffer = new Buffer(0);
                        break;
                    case 1:
                        buffer = buffers[0];
                        break;
                    default:
                        buffer = new Buffer(nread);
                        for (let i = 0, pos = 0, l = buffers.length; i < l; i++) {
                            let chunk = buffers[i];
                            chunk.copy(buffer, pos);
                            pos += chunk.length;
                        }
                        break;
                }

                let content = buffer.toString();
                let log = content.replace(DATA_TAG_REG, '');
                let data = decode(content);

                if (log) {
                    process.stdout.write(log);
                }

                if (data) {
                    callback(null, data);
                }

            });


            child.stderr.on('data', content => {
                process.stderr.write(content);
            });


            child.on('exit', code => {
                if (code === 0) {
                    callback(null);
                } else {
                    callback(new Error(`"${command}": child process exited with code ${code}.`));
                }
            });


            // 解决 node timeout 不会走异常流程的问题
            if (options.timeout) {
                timer = setTimeout(() => {
                    callback(new Error(`"${command}": child process timeout.`));
                }, options.timeout);
            }

        }).catch(reject);
    });
};



/**
 * 向父进程发送消息 - 子进程只有一次机会发送消息，发送完毕后会被父进程关闭
 * worker.js 是使用 childProcess.exec() 来运行的子进程，
 * nodejs 没有为 exec() 提供进程之间发送消息的方法，所以这里创建私有协议来通讯
 * @param   {Object}    data    JSON 数据
 */
worker.send = (data) => {
    if (process.env[WORKER_ENV] === WORKER_ENV) {
        process.stdout.write(encode(data));
    }
};


module.exports = worker;
@@ -1198,20 +1198,21 @@
 cess.env
+ 
 %7D);%0A
-
 %0A       
@@ -1644,16 +1644,17 @@
 (() =%3E %7B
+ 
 %7D);%0A    
@@ -1855,32 +1855,60 @@
  timer = null;%0A%0A
+            let cache = '';%0A
             let 
@@ -1928,65 +1928,8 @@
 lse;
-%0A            let buffers = %5B%5D;%0A            let nread = 0;
 %0A%0A  
@@ -2501,17 +2501,62 @@
      %7D%0A%0A
+            child.stdout.setEncoding('utf8');
 %0A
-
         
@@ -2634,58 +2634,8 @@
 ) %7B%0A
-                    content = content.toString();%0A
     
@@ -2769,56 +2769,13 @@
     
-buffers.push(content);%0A                    nread
+cache
  += 
@@ -2781,23 +2781,16 @@
  content
-.length
 ;%0A      
@@ -3042,753 +3042,19 @@
 let 
-buffer = null;%0A%0A                switch (buffers.length) %7B%0A                    case 0:%0A                        buffer = new Buffer(0);%0A                        break;%0A                    case 1:%0A                        buffer = buffers%5B0%5D;%0A                        break;%0A                    default:%0A                        buffer = new Buffer(nread);%0A                        for (let i = 0, pos = 0, l = buffers.length; i %3C l; i++) %7B%0A                            let chunk = buffers%5Bi%5D;%0A                            chunk.copy(buffer, pos);%0A                            pos += chunk.length;%0A                        %7D%0A                        break;%0A                %7D%0A%0A                let content = buffer.toString();%0A                let log = content
+log = cache
 .rep
@@ -3112,22 +3112,20 @@
 decode(c
-ontent
+ache
 );%0A%0A    

重构worker，降低复杂度