const vscode_debugadapter = require('vscode-debugadapter');
const Path = require('path');
const topjsRuntime = require('./topjsRuntime');
const { Subject } = require('await-notify');
const spawn = require('child_process');
const process = require('process');
const fs = require('fs');
const os = require('os');
const cmd_exists = require('command-exists').sync;
const vscode = require('vscode');

function fold_varg(...args) {
    let buf = [];
    for(let i = 0; i < args.length; ++i) {
        if(typeof args[i] === 'object') {
            try {
                buf.push(Buffer.from(JSON.stringify(args[i], null, ' ')));
            } catch(e) {
                buf.push(Buffer.from(''+args[i]));
            }
        } else {
            buf.push(Buffer.from(''+args[i]));
        }
    }
    return Buffer.concat(buf).toString();
}

class TopJSDebugSession extends vscode_debugadapter.LoggingDebugSession {
    /**
     * this class re-write necessary requests of base class, and register
     * necessary callback handlers for interacting with debugger backend.
     */
    constructor() {
        // setup log file path for base class
        super(Path.join(os.tmpdir(), "topjs-debug.txt"));
        // for variable inspection
        this._variablehandles = new vscode_debugadapter.Handles();
        // for async event notify
        this._configurationDone = new Subject();
        // source file line start at 0
        this.setDebuggerLinesStartAt1(false);
        // source file column start at 0
        this.setDebuggerColumnsStartAt1(false);

        this._cachedBreakpoints = {};
        this._restart = false;

        // overwrite event in base class

        // when close stop button
        this.on('close', () => {
            this.terminateSession('exit');
            super.shutdown();
        });

        this.on('error', (error) => {
            this.terminateSession('exit');
            super.shutdown();
        });

        // runtime instance for interacting with debugger backend
        this._runtime = new topjsRuntime.topjsRuntime();
        this._runtime.setLogger(this);

        // runtime recieved 'stopOnEntry' event from debugger backend, then
        // forward to vscode gui by emitting event via debugger adapter
        this._runtime.on(topjsRuntime.dEvents.stopped, (data) => {
            this.sendEvent(new vscode_debugadapter.StoppedEvent(data.body.reason, TopJSDebugSession.THREAD_ID));
        });

        this._runtime.on(topjsRuntime.dEvents.continued, (data) => {
            this.sendEvent(new vscode_debugadapter.ContinuedEvent(TopJSDebugSession.THREAD_ID));
        });

        this._runtime.on(topjsRuntime.dEvents.terminated, (data) => {
            this.sendEvent(new vscode_debugadapter.TerminatedEvent(false)); // don't restart
        });

        this._runtime.on(topjsRuntime.dEvents.output, (data) => {
            const e = new vscode_debugadapter.OutputEvent(`${data.body.output}\n`, data.body.category);
            this.sendEvent(e);
        });

        this._runtime.on(topjsRuntime.dEvents.breakpoint, (data) => {
            this.sendEvent(new vscode_debugadapter.BreakpointEvent(data.body.reason, {
                verified: data.body.breakpoint.verified,
                id: data.body.breakpoint.id
            }));
        });

        this._runtime.on(topjsRuntime.dEvents.end, (body) => {
            this.sendEvent(new vscode_debugadapter.TerminatedEvent());
        });

        this._runtime.on('connected', () => {
            this.setBreakPointsRequest(this._cachedBreakpoints.response, this._cachedBreakpoints.args);
        });
    }

    _log_impl(type, ...args) {
        const s = fold_varg(...args);
        const e = new vscode_debugadapter.OutputEvent(`${s}`, type);
        this.sendEvent(e);
    }

    log(...args) {
        this._log_impl('console', ...args);
    }

    info(...args) {
        this._log_impl('stdout', ...args);
    }

    err(...args) {
        this._log_impl('stderr', ...args);
    }

    createSource(filepath) {
        // backend must return absolute path of debugging script.
        if(!fs.existsSync(filepath)) {
            if(this._program.indexOf(filepath) >= 0) {
                filepath = this._program;
            }
        }
        return new vscode_debugadapter.Source(Path.basename(filepath), this.convertDebuggerPathToClient(filepath), undefined, undefined, 'topjs-adapter-data');
    }

    /**
     * response: initial configuration report debugger capabilities
     * args: ignored in base class
     */
    initializeRequest(response, args) {
        response.body = response.body || {};
        response.body.supportsConfigurationDoneRequest = true;
        // This default debug adapter does not support hovers based on the 'evaluate' request.
        response.body.supportsEvaluateForHovers = false;
        // This default debug adapter does not support the 'restart' request.
        // if false, adapter will terminate process and spawn a new one.
        response.body.supportsRestartRequest = true;
        this.sendResponse(response);
        this.sendEvent(new vscode_debugadapter.InitializedEvent());
    }

    /**
     * we have to overwrite this function, since we set true in initializeRequest
     */
    configurationDoneRequest(response, args) {
        super.configurationDoneRequest(response, args);
        this._configurationDone.notify(); // notifiy launchRequest
    }

    isExtensionHost(args) {
        return args.adapterID === 'extensionHost2' || args.adapterID === 'extensionHost';
    }

    terminateSession(reason) {
        const processId = this._backend.pid;
        if (process.platform === 'win32') {
            const windir = process.env['WINDIR'] || 'C:\\Windows';
            const TASK_KILL = Path.join(windir, 'System32', 'taskkill.exe');
            // when killing a process in Windows its child processes are *not* killed but become root processes.
            // Therefore we use TASKKILL.EXE
            try {
                spawn.execSync(`${TASK_KILL} /F /T /PID ${processId}`);
            }
            catch (err) {
                //this.err(err.message);
            }
        }
        else {
            // on linux and OS X we kill all direct and indirect child processes as well
            try {
                const cmd = 'kill';
                spwan.spawnSync(cmd, [processId.toString()]);
            }
            catch (err) {
            }
        }
    }

    spawnBackend(args) {
        let argv = [`--remote-debugging-port=${args.port}`, args.program];
        this._backend = spawn.spawn(args.runtimeExecutable, argv);
        this._backend.on('exit', () => {
            if(!this._restart) {
                this._restart = false;
                this.sendEvent(new vscode_debugadapter.TerminatedEvent());
            }
            if(this.isExtensionHost()) {
                this.terminateSession(msg);
            }
        });
        this._backend.on('close', (code) => {
            const msg = `debugger backend exit with code ${code}`;
            if(!this.isExtensionHost()) {
                this.terminateSession(msg);
            }
            if(!this._restart) {
                this._restart = false;
                this.sendEvent(new vscode_debugadapter.TerminatedEvent());
            }
            process.exit(0);
        });
        this._backend.stdout.on('data', (data) => {
            this.log(data.toString());
        });
        this._backend.stderr.on('data', (data) => {
            this.log(data.toString());
        });
        this._backend.on('error', (err) => {
            this.err(err.message());
            this.err('main process exit...');
            if(!this._restart) {
                this._restart = false;
                this.sendEvent(new vscode_debugadapter.TerminatedEvent());
            }
            process.exit(1);
        });
        this.log([args.runtimeExecutable, `--remote-debugging-port=${args.port}`, Path.basename(args.program), '\n'].join(' '));
    }

    validateArgs(args) {
        if(!fs.existsSync(args.runtimeExecutable) && !cmd_exists(args.runtimeExecutable)) {
            this.err(`${args.runtimeExecutable} not found`);
            return false;
        }
        if(!args.program) {
            this.err('no program found, please add "program" filed in "launch.json"');
            return false;
        }
        if(!fs.existsSync(args.program)) {
            this.err(`can't find file: ${args.program}`);
            return false;
        }
        if(!args.program.endsWith('.js')) {
            this.err(`${args.program} is not a javascript file`);
            return false;
        }
        return true;
    }

    launchRequest(response, args) {
        if(!args.runtimeExecutable) {
            args.runtimeExecutable = 'topjs3';
        }
        if(!args.port) {
            args.port = 30992;
        }
        if(!this.validateArgs(args)) {
            this.sendEvent(new vscode_debugadapter.TerminatedEvent());
        } else {
            this._args = args;
            this._program = args.program;
            vscode_debugadapter.logger.setup(args.trace ? vscode_debugadapter.Logger.LogLevel.Verbose : vscode_debugadapter.Logger.LogLevel.Stop, false);
            this.spawnBackend(args);
            this._configurationDone.wait().then(() => {
                this._runtime.start(args);
                this.sendResponse(response);
            });
        }
    }

    disconnectRequest(response, args) {
        if(this.isExtensionHost()) {
            this.terminateSession('disconnected');
        }
        this._runtime.quit();
        super.disconnectRequest(response, args);
    }

    /// below is the real debugging operation we support
    setBreakPointsRequest(response, args) {
        if(!this._runtime || !this._runtime.isStarted()) {
            this._cachedBreakpoints = {"response": response, "args": args};
            response.body.breakpoints = [];
            response.success = true;
            return this.sendRequest(response);
        }
        // args.line is deprecated, ignore.
        args.breakpoints = args.breakpoints.map(b => {
            b.line = this.convertClientLineToDebugger(b.line);
            return b;
        });
        this._runtime.setBreakpointsRequest(args, response, (resp, breakpoints) => {
            // breakpoints is a JSON object
            if(!breakpoints.success) {
                this.sendErrorResponse(new vscode_debugadapter.Response(breakpoints), JSON.stringify(breakpoints));
                return;
            }
            const r = breakpoints.body.breakpoints.map(breakpoint => {
                let {verified, line, id} = breakpoint;
                const bp = new vscode_debugadapter.Breakpoint(verified, this.convertDebuggerLineToClient(line));
                bp.id = id;

                return bp;
            });
            resp.body = {
                breakpoints: r
            }
            this.sendResponse(resp); // send to frontend
        });
    }

    continueRequest(response, args) {
        // args.threadId is ignore, since we only support singel thread debugging
        this._runtime.continueRequest(args, response, (resp, data) => {
            resp.body = {
                threadId: TopJSDebugSession.THREAD_ID, // ignore response id
                allThreadsContinued: data
            };
            this.sendResponse(resp);
        });
    }

    pauseRequest(response, args) {
        this._runtime.pauseRequest(args, response, (resp, data) => {
            this.sendResponse(resp);
        });
    }

    // if not support, treat as nextRequest, after that, a `stopped` event must
    // return from backend
    stepInRequest(response, args) {
       this._runtime.stepInRequest(args, response, (resp, data) => {
           this.sendResponse(resp);
       });
    }

    stepOutRequest(response, args) {
        this._runtime.stepOutRequest(args, response, (resp, data) => {
            this.sendResponse(resp);
        });
    }

    // The debug adapter first sends the response and then a ‘stopped’
    // event (with reason ‘step’) after the step has completed.
    nextRequest(response, args) {
        this._runtime.nextRequest(args, response, (resp, data) => {
            this.sendResponse(resp);
        });
    }

    // Retrieves all child variables for the given variable reference.
    variablesRequest(response, args) {
        //const id = this._variablehandles.get(args.variablesReference);
        // what's variablesReference ???
        // it's obtain from `scopesRequest` which is contextId.
        this._runtime.variablesRequest(args, response, (resp, data) => {
            resp.body = {
                variables: data.body.variables
            };
            this.sendResponse(resp);
        });
    }

    // The request returns the variable scopes for a given stackframe ID.
    scopesRequest(response, args) {
        this._runtime.scopesRequest(args, response, (resp, data) => {
            // data is a JSON object
            const s = data.body.scopes.map(l => {
                const scope = new vscode_debugadapter.Scope(l.name, l.variablesReference);
                scope.expensive = l.expensive;
                return scope;
            });
            resp.body = {
                scopes: s // The scopes of the stackframe. If the array has length zero, there are no scopes available.
            };
            this.sendResponse(resp);
        })
    }

    stackTraceRequest(response, args) {
        this._runtime.stackTraceRequest(args, response, (resp, data) => {
            // data is a JSON object
            const frames = data.body.stackFrames.map(f => {
                const frame = new vscode_debugadapter.StackFrame(f.id, f.name, this.createSource(f.file), this.convertDebuggerLineToClient(f.line));
                return frame;
            });
            resp.body = {
                stackFrames: frames,
                totalFrames: frames.count
            };
            this.sendResponse(resp);
        });
    }

    restartRequest(response, args) {
        this._restart = true;
        this._runtime.quit();
        this.terminateSession('restart');
        this.spawnBackend(this._args);
        this._runtime.start(this._args);
        this.sendResponse(response);
    }

    // we don't support multi-thread debugging, simply answer static TopJSDebugSession.THREAD_ID
    threadsRequest(response, args) {
        response.body = {
            threads: [
                new vscode_debugadapter.Thread(TopJSDebugSession.THREAD_ID, "thread 1")
            ]
        };
        this.sendResponse(response);
    }

    // low priority
    // TODO: implement this
    evaluateRequest(response, args) {
        // this._runtime.evaluateRequest(args)
        response.body = {
            result: undefined,
            variablesReference: 0
        };
        this.sendResponse(response);
    }
}

// we don't support multi-thread debugging, so hard code to 1
TopJSDebugSession.THREAD_ID = 1;

exports.TopJSDebugSession = TopJSDebugSession;