/**
 * Debugger support for skulpt module
 */

var Sk = Sk || {}; //jshint ignore:line

function hasOwnProperty(obj, prop) {
    var proto = obj.constructor.prototype;
    return (prop in obj) &&
        (!(prop in proto) || proto[prop] !== obj[prop]);
}

Sk.Breakpoint = function(filename, lineno, colno) {
    this.filename = filename;
    this.lineno = lineno;
    this.colno = colno;
    this.enabled = true;
    this.ignore_count = 0;
};

Sk.Debugger = function(filename, output_callback) {
    this.dbg_breakpoints = {};
    this.tmp_breakpoints = {};
    this.suspension_stack = [];
    this.current_suspension = -1;
    this.eval_callback = null;
    this.suspension = null;
    this.output_callback = output_callback;
    this.step_mode = false;
    this.filename = filename;
};

Sk.Debugger.prototype.print = function(txt) {
    if (this.output_callback != null) {
        this.output_callback.print(txt);
    }
};

Sk.Debugger.prototype.get_source_line = function(lineno) {
    if (this.output_callback != null) {
        return this.output_callback.get_source_line(lineno);
    }
    
    return "";
};

Sk.Debugger.prototype.move_up_the_stack = function() {
    this.current_suspension = Math.min(this.current_suspension + 1, this.suspension_stack.length - 1);
};

Sk.Debugger.prototype.move_down_the_stack = function() {
    this.current_suspension = Math.max(this.current_suspension - 1, 0);
};

Sk.Debugger.prototype.enable_step_mode = function() {
    this.step_mode = true;
};

Sk.Debugger.prototype.disable_step_mode = function() {
    this.step_mode = false;
};

Sk.Debugger.prototype.get_suspension_stack = function() {
    return this.suspension_stack;
};

Sk.Debugger.prototype.get_active_suspension = function() {
    if (this.suspension_stack.length === 0) {
        return null;
    }

    return this.suspension_stack[this.current_suspension];
};

Sk.Debugger.prototype.generate_breakpoint_key = function(filename, lineno, colno) {
    var key = filename + "-" + lineno;
    return key;
};

Sk.Debugger.prototype.check_breakpoints = function(filename, lineno, colno, globals, locals) {
    // If Step mode is enabled then ignore breakpoints since we will just break
    // at every line.
    if (this.step_mode === true) {
        return true;
    }
    
    var key = this.generate_breakpoint_key(filename, lineno, colno);
    if (hasOwnProperty(this.dbg_breakpoints, key) &&
        this.dbg_breakpoints[key].enabled === true) {
        var bp = null;
        if (hasOwnProperty(this.tmp_breakpoints, key)) {
            delete this.dbg_breakpoints[key];
            delete this.tmp_breakpoints[key];
            return true;
        }
        
        this.dbg_breakpoints[key].ignore_count -= 1;
        this.dbg_breakpoints[key].ignore_count = Math.max(0, this.dbg_breakpoints[key].ignore_count);
        
        bp = this.dbg_breakpoints[key];
        if (bp.ignore_count === 0) {
            return true;
        } else {
            return false;
        }
    }
    return false;
};

Sk.Debugger.prototype.get_breakpoints_list = function() {
    return this.dbg_breakpoints;
};

Sk.Debugger.prototype.disable_breakpoint = function(filename, lineno, colno) {
    var key = this.generate_breakpoint_key(filename, lineno, colno);
    
    if (hasOwnProperty(this.dbg_breakpoints, key)) {
        this.dbg_breakpoints[key].enabled = false;
    }
};

Sk.Debugger.prototype.enable_breakpoint = function(filename, lineno, colno) {
    var key = this.generate_breakpoint_key(filename, lineno, colno);
    
    if (hasOwnProperty(this.dbg_breakpoints, key)) {
        this.dbg_breakpoints[key].enabled = true;
    }
};

Sk.Debugger.prototype.clear_breakpoint = function(filename, lineno, colno) {
    var key = this.generate_breakpoint_key(filename, lineno, colno);
    if (hasOwnProperty(this.dbg_breakpoints, key)) {
        delete this.dbg_breakpoints[key];
        return null;
    } else {
        return "Invalid breakpoint specified: " + filename + " line: " + lineno;
    }
};

Sk.Debugger.prototype.clear_all_breakpoints = function() {
    this.dbg_breakpoints = {};
    this.tmp_breakpoints = {};
};

Sk.Debugger.prototype.set_ignore_count = function(filename, lineno, colno, count) {
    var key = this.generate_breakpoint_key(filename, lineno, colno);
    if (hasOwnProperty(this.dbg_breakpoints, key)) {
        var bp = this.dbg_breakpoints[key];
        bp.ignore_count = count;
    }
};

Sk.Debugger.prototype.set_condition = function(filename, lineno, colno, lhs, cond, rhs) {
    var key = this.generate_breakpoint_key(filename, lineno, colno);
    var bp;
    if (hasOwnProperty(this.dbg_breakpoints, key)) {
        // Set a new condition
        bp = this.dbg_breakpoints[key];
    } else {
        bp = new Sk.Breakpoint(filename, lineno, colno);
    }
    
    bp.condition = new Sk.Condition(lhs, cond, rhs);
    this.dbg_breakpoints[key] = bp;
};

Sk.Debugger.prototype.print_suspension_info = function(suspension) {
    var filename = suspension.filename;
    var lineno = suspension.lineno;
    var colno = suspension.colno;
    this.print("Hit Breakpoint at <" + filename + "> at line: " + lineno + " column: " + colno + "\n");
    this.print("----------------------------------------------------------------------------------\n");
    this.print(" ==> " + this.get_source_line(lineno - 1) + "\n");
    this.print("----------------------------------------------------------------------------------\n");
};

Sk.Debugger.prototype.set_suspension = function(suspension) {
    var parent = null;
    if (!hasOwnProperty(suspension, "filename") && suspension.child instanceof Sk.misceval.Suspension) {
        suspension = suspension.child;
    }
        
    // Pop the last suspension of the stack if there is more than 0
    if (this.suspension_stack.length > 0) {
        this.suspension_stack.pop();
        this.current_suspension -= 1;
    }
    
    // Unroll the stack to get each suspension.
    while (suspension instanceof Sk.misceval.Suspension) {
        parent = suspension;
        this.suspension_stack.push(parent);
        this.current_suspension += 1;
        suspension = suspension.child;
    }

    suspension = parent;
    
    this.print_suspension_info(suspension);
};

Sk.Debugger.prototype.add_breakpoint = function(filename, lineno, colno, temporary) {
    var key = this.generate_breakpoint_key(filename, lineno, colno);
    this.dbg_breakpoints[key] = new Sk.Breakpoint(filename, lineno, colno);
    if (temporary) {
        this.tmp_breakpoints[key] = true;
    }
};

Sk.Debugger.prototype.suspension_handler = function(susp) {
    return new Promise(function(resolve, reject) {
        try {
            resolve(susp.resume());
        } catch(e) {
            reject(e);
        }
    });
};

Sk.Debugger.prototype.resume = function() {
    // Reset the suspension stack to the topmost
    this.current_suspension = this.suspension_stack.length - 1;
    
    if (this.suspension_stack.length === 0) {
        this.print("No running program");
    } else {
        var promise = this.suspension_handler(this.get_active_suspension());
        promise.then(this.success.bind(this), this.error.bind(this));
    }
};

Sk.Debugger.prototype.pop_suspension_stack = function() {
    this.suspension_stack.pop();
    this.current_suspension -= 1;
};

Sk.Debugger.prototype.success = function(r) {
    if (r instanceof Sk.misceval.Suspension) {
        this.set_suspension(r);
    } else {
        if (this.suspension_stack.length > 0) {
            // Current suspension needs to be popped of the stack
            this.pop_suspension_stack();
            
            if (this.suspension_stack.length === 0) {
                this.print("Program execution complete");
                return;
            }
            
            var parent_suspension = this.get_active_suspension();
            // The child has completed the execution. So override the child's resume
            // so we can continue the execution.
            parent_suspension.child.resume = function() {
                return r;
            };
            this.resume();
        } else {
            this.print("Program execution complete");
        }
    }
};

Sk.Debugger.prototype.error = function(e) {
    this.print("Traceback (most recent call last):");
    for (var idx = 0; idx < e.traceback.length; ++idx) {
        this.print("  File \"" + e.traceback[idx].filename + "\", line " + e.traceback[idx].lineno + ", in <module>");
        var code = this.get_source_line(e.traceback[idx].lineno - 1);
        code = code.trim();
        code = "    " + code;
        this.print(code);
    }
    
    var err_ty = e.constructor.tp$name;
    for (idx = 0; idx < e.args.v.length; ++idx) {
        this.print(err_ty + ": " + e.args.v[idx].v);
    }
};

Sk.Debugger.prototype.asyncToPromise = function(suspendablefn, suspHandlers, debugger_obj) {
    return new Promise(function(resolve, reject) {
        try {
            var r = suspendablefn();

            (function handleResponse (r) {
                try {
                    while (r instanceof Sk.misceval.Suspension) {
                        debugger_obj.set_suspension(r);
                        return;
                    }

                    resolve(r);
                } catch(e) {
                    reject(e);
                }
            })(r);

        } catch (e) {
            reject(e);
        }
    });
};

Sk.Debugger.prototype.execute = function(suspendablefn, suspHandlers) {
    var r = suspendablefn();
    
    if (r instanceof Sk.misceval.Suspension) {
        this.suspensions.concat(r);
        this.eval_callback(r);
    }
};

goog.exportSymbol("Sk.Debugger", Sk.Debugger);
