import _ from 'lodash';
import childProcess, { ChildProcessWithoutNullStreams, SpawnOptionsWithoutStdio } from 'child_process';
import stripAnsi from 'strip-ansi';
import { Logger } from './logger-types';

export type LogTarget = 'stdout' | 'stderr' | 'error';

export interface LogProcessOptions {
    onLog?: {
        callback: (name: string, target: LogTarget, line: string) => void;
        pattern: RegExp;
        target: LogTarget;
    }[];
    info?: Record<string, any>;
}

interface LogBufferHandler {
    handler: (target: LogTarget, data: string) => void;
    flush: () => void;
}

export function createLogBufferHandler(name: string, logger: Logger, { onLog, info = {} }: LogProcessOptions = {}): LogBufferHandler {
    const lineBuffer: Record<LogTarget, string> = {
        stdout: '',
        stderr: '',
        error: ''
    };
    function logLine(target: LogTarget, line: string) {
        if (_.isEmpty(_.trim(stripAnsi(line)))) {
            return;
        }
        if (target === 'stdout') {
            logger.debug(`[${name}]: ${line}`, info);
        } else {
            // log.error - too much errors logs generated which are not really errors
            logger.debug(`[${name}]: ${line}`, info);
        }
        _.forEach(onLog, (onLogItem) => {
            if (_.has(onLogItem, 'callback')) {
                const pattern = _.get(onLogItem, 'pattern');
                if (pattern && !pattern.test(stripAnsi(line))) {
                    return;
                }
                const filterTarget = _.get(onLogItem, 'target', null);
                if (filterTarget && filterTarget !== target) {
                    return;
                }
                onLogItem.callback(name, target, line);
            }
        });
    }

    return {
        // If handler gets unterminated line, save it until we get line ending
        // For example assume data stream has two lines called in following order:
        // handler("hello world\nhow are ")
        // handler("you?\n")
        // First time the handler called, we can output the first line: "hello world".
        // The second line was not terminated so we need to wait until we receive its ending
        // then we can output the full second line: "how are you?"
        handler: (target: LogTarget, data: string) => {
            const lines = _.split(data, '\n');
            if (lineBuffer[target]) {
                lines[0] = lineBuffer[target] + lines[0];
                lineBuffer[target] = '';
            }
            if (_.last(lines) !== '') {
                lineBuffer[target] = lines.pop()!;
            }
            _.forEach(lines, (line) => {
                logLine(target, line);
            });
        },
        flush: () => {
            _.forEach(lineBuffer, (line, target) => {
                const logTarget = target as LogTarget;
                logLine(logTarget, line);
                lineBuffer[logTarget] = '';
            });
        }
    };
}

export function logProcess(p: ChildProcessWithoutNullStreams, name: string, logger: Logger, options?: LogProcessOptions) {
    const bufferHandler = createLogBufferHandler(name, logger, options);
    p.stdout.on('data', _.partial(bufferHandler.handler, 'stdout'));
    p.stderr.on('data', _.partial(bufferHandler.handler, 'stderr'));
    p.on('error', (error) => {
        logger.debug(`[${name}] error: ${error.message}`);
    });
    p.on('close', (code) => {
        bufferHandler.flush();
        if (code != 0) {
            logger.debug(`[${name}] closed with code: ${code}`);
        }
    });
    p.on('exit', (code) => {
        bufferHandler.flush();
        if (code != 0) {
            logger.debug(`[${name}] exited with code: ${code}`);
        }
    });
}

export function getProcessPromise(p: ChildProcessWithoutNullStreams): Promise<string> {
    return new Promise((resolve, reject) => {
        let data = '';
        let stderr = '';
        p.stdout.on('data', (out) => (data += out));
        p.stderr.on('data', (out) => (stderr += out));
        p.on('close', (code) => {
            if (code !== 0) {
                reject(new Error(`process exited with code: ${code}, stderr: ${stderr}`));
            } else {
                resolve(data);
            }
        });
        p.on('error', (err) => {
            reject(err);
        });
    });
}
