function StackManager(rest_interface) {
    var frames = this.frames = [];

    // Return one particular frame
    this.frame = function(i) {
        return frames[i];
    };

    // Reload one particular frame
    this.reloadFrame = function(i) {
        var d = $.Deferred();
        rest_interface.stackFrame(i)
            .done(function(frame) {
                frames[i] = frame;
                d.resolve(frame);
            });
        return d.promise();
    };

    var _insert_frame_from_new = function(frameno, frame, frames) {
        frame.frameno = frameno;
        var frame_obj = new StackFrame(frame);
        frames[frameno] = frame_obj;
        return frame_obj;
    };

    // Reload all frames or initialize from new
    this.initialize = function() {
        var d = $.Deferred();
        frames.splice(0, frames.length); // Clear the list
        rest_interface.stack()
            .done(function(stack) {
                for(var i = 0; i < stack.length; i++) {
                    var frame_obj = _insert_frame_from_new(i, stack[i], frames);
                    d.notify(frame_obj, null, i);
                }

                rest_interface.status({next_statement: 1})
                              .done(function(status) {
                                    d.resolve(status);
                                })
            });
        return d.promise();
    };

    // Update our representation of the stack efficiently using HEAD requests
    // when possible and GET requests when neessary
    this.update = function() {
        var d = $.Deferred(),
            prev_frames_by_serial = {};

        for (var i = 0; i < frames.length; i++) {
            var serial = frames[i].serial;
            prev_frames_by_serial[serial] = frames[i];
        }

        var new_frames = [];

        var finalize = function() {
            // Got responses from all the stack frames we had before.
            // Anything else left in prev_frames_by_serial are gone
            for (var serial in prev_frames_by_serial) {
                d.notify(prev_frames_by_serial[serial], prev_frames_by_serial[serial].frameno, null);
            }
            frames = new_frames;
            d.resolve();
        };

        function update_one_frame(frame, i) {
            var this_frame_serial = frame.serial;
            frame.frameno = i;
            new_frames[i] = frame;
            if (this_frame_serial in prev_frames_by_serial) {
                // this frame existed before
                var prev_frame = prev_frames_by_serial[this_frame_serial];

                if ((prev_frame.frameno !== frame.frameno)
                    || (prev_frame.line !== frame.line)
                ) {
                    d.notify(frame, prev_frame.frameno, i);  // Only notify if something changed
                }
                delete prev_frames_by_serial[this_frame_serial];

            } else {
                // This is a new frame that did not exist before
                d.notify(frame, null, i);
            }
        };

        rest_interface.stackNoArgs()
            .fail(function(jqxhr, text_status, error_thrown) {
                alert('Error getting stack: '+error_thrown);
            })
            .done(function(stackframes) {
                for (var i = (stackframes.length - 1); i >= 0; i--) {
                    var frame = new StackFrame(stackframes[i]);
                    update_one_frame(frame, i);
                };
                finalize();
            });
        return d.promise();
    };

    this.updateFrameWithArgs = function(frame) {
        var frameno = frame.frameno,
            d = $.Deferred();

        rest_interface.stackFrame(frameno)
            .fail(d)
            .done(function(new_frame_data) {
                var new_frame = new StackFrame(new_frame_data);
                frame.args = new_frame.args;
                d.resolve(frame);
            });

        return d.promise();
    };
}

function StackFrame(params) {
    var frame = this;
    ['package','filename','line','subroutine','subname','hasargs',
     'wantarray', 'evaltext', 'is_require','hints','bitmask','href',
     'evalfile','evalline','autoload','level','serial','frameno'
    ].forEach(function(key) {
        frame[key] = params[key];
    });

    var args = [];
    this.args = args;
    if(params.args) {
        for(var i = 0; i < params.args.length; i++) {
            args[i] = PerlValue.parseFromEval(params.args[i]);
        }
    }

    if (this.wantarray === null) {
        this.sigil = '';
    } else if (this.wantarray) {
        this.sigil = '@';
    } else {
        this.sigil = '$';
    }
}
