import { workspace } from 'vscode';
import { spawn } from 'child_process';
import { EventEmitter } from 'events';
import { CreateQuestionAnswer } from './QuestionHandler';

/** 
 * Class for communicating with the installed RefactorErl
 * The installed RefactorErl is spawned as a Child Process
 */
export class RefactorErlProcess{
    private Cmd_Process:any;

    /** Launch configurations for the command line child process */
    private _config:{ LaunchCommand:string, RefErlDir:string|undefined,
                    LaunchArgs:string[], isShell:boolean };
    private _currentCallback:any = undefined;
    private _isRunning:boolean = false;
    private _waitingForResponse:boolean = false;

    /** Used for sending events to the extension */
    public RefErlEvents:EventEmitter = new EventEmitter();

    constructor(){
        this._config = {
            LaunchCommand: process.platform === "win32" ? 'bin\\referl' : 'bin/referl',
            RefErlDir: workspace.getConfiguration('RefactorErl').get('BasePath'),
            LaunchArgs:workspace.getConfiguration('RefactorErl').get('ConnectToRunningServer') ?
                    ['-client', '-sname', 'vscode@localhost'] : [],
            isShell: process.platform === "win32"
        };
        workspace.onDidChangeConfiguration(() => {
            this._config.RefErlDir = workspace.getConfiguration('RefactorErl').get('BasePath');
            this._config.LaunchArgs = workspace.getConfiguration('RefactorErl').get('ConnectToRunningServer') ?
                    ['-client', '-sname', 'vscode@localhost'] : [];
        });
    }

    /**
     * Launch or Stop the child process
     * If launched, it also initializes the event listerens for the command line process
    */
    public async toggle():Promise<void>{
        if(this._isRunning){ this.Cmd_Process.kill(); return; }
        this.Cmd_Process = spawn(this._config.LaunchCommand, this._config.LaunchArgs, {
                            cwd: this._config.RefErlDir,
                            shell:this._config.isShell}
                        );
        this._isRunning = true;
        this._waitingForResponse = true;

        this.InitEventListeners();

        await sleep(500);
        if(this._isRunning){
            this._waitingForResponse = false;
            this.Cmd_Process.stdin.write('referl_vscode:run_server().\n');
            this.RefErlEvents.emit('StateChange', {
                Col:"LightGreen",
                Msg:"RefactorErl is running",
                Prompt:"Turn off RefactorErl"
            });
        }
    }

    /** Force stops the command line process */
    public ForceStop():void{
        if(this._isRunning){
            this.Cmd_Process.kill(9);
        }
    }

    /**
     * Sends a reqeust to the installed RefactorErl
     * @param Request Request string
     * @param Callback Optional. Function called upon receiving the response from RefactorErl
     */
    public Request(Request:string, Callback?:(Log: any) => {Event:string, Data:any}):void{
        if(!this.CanRequest()){ return; }
        if(Callback !== undefined){
            this._waitingForResponse = true;
            this._currentCallback = Callback;
        }
        this.Cmd_Process.stdin.write(Request);
    }

    /**
     * Handles questions from RefactorErl
     * @param Input Received question
     */
    private async ProcessQuestion(Input:any):Promise<void>{
        let Answer = await CreateQuestionAnswer(Input);
        this.Cmd_Process.stdin.write(Answer);
    }

    /**
     * Handles the replies from RefactorErl
     * @param Input Received reply
     */
    private ProcessReply(Input:any):void{
        if(this._waitingForResponse){
            try{
                let Result = this._currentCallback(Input);
                this.RefErlEvents.emit(Result.Event, Result.Data);
            }catch(e){
                this.RefErlEvents.emit("error", "Error happened during the request.");
            }
            this._waitingForResponse = false;
        }
        else{ this.RefErlEvents.emit('output', JSON.stringify(Input)); }
    }

    /** Checks if the user allowed to send requests */
    private CanRequest():boolean{
        if(!this._isRunning){
            this.RefErlEvents.emit('error', "RefactorErl is not running.");
            return false;
        }
        if(this._waitingForResponse){
            this.RefErlEvents.emit('error', "Another request is in progress.");
            return false;
        }
        return true;
    }

    /** Initializes the event listeners for the command line process */
    private InitEventListeners():void{
        this.Cmd_Process.on('error', (arg:any) => {
            this.RefErlEvents.emit('error', "Could not start RefactorErl. Please check your settings");
        });

        this.Cmd_Process.stdout.on('data', (arg:string) => {
            let Resp = FormatArg(`${arg}`);
            if(IsJsonString(Resp)){
                let RespJson = JSON.parse(Resp)["Response"];
                if(RespJson[0] === "error"){
                    this.RefErlEvents.emit('error', JSON.stringify(RespJson));
                    this._waitingForResponse=false;
                }
                else if(RespJson[1] === "reply"){ this.ProcessReply(RespJson[2]); }
                else if(RespJson[1] === "question"){ this.ProcessQuestion(RespJson); }
            }
        });
        
        this.Cmd_Process.on('close', (code:any) => {
            this._isRunning = false;
            if(`${code}` === 'null'){
                this.RefErlEvents.emit('StateChange', {
                    Col:"Red",
                    Msg:"RefactorErl is turned off",
                    Prompt:"Turn on RefactorErl"
                });
            }
        });
    }
}

/**
 * Makes the extension wait for the given miliseconds
 * @param ms Amount of miliseconds to wait
 */
function sleep(ms:number){
    return new Promise(resolve => {
        setTimeout(resolve, ms);
    });
}

/**
 * Checks if a string can be parsed into a JSON object
 * @param str String to check
 * @returns True if it can be parsed, otherwise false
 */
function IsJsonString(str:string):boolean{
    try {
        JSON.parse(str);
    } catch (e) {
        return false;
    }
    return true;
}

/**
 * Removes control chars & empty list objects from received message
 * @param arg String to format
 * @returns Formatted string
 */
function FormatArg(arg:string):string{
    return arg.replace(new RegExp("'", 'g'), '"')
            .replace(new RegExp(", ]", 'g'), ",\"\"]")
            .replace(/[\x00-\x1F\x7F-\x9F]/g, "");
}