iD.Graph = function(entities) {
    if (!(this instanceof iD.Graph)) return new iD.Graph(entities);

    if (_.isArray(entities)) {
        this.entities = {};
        for (var i = 0; i < entities.length; i++) {
            this.entities[entities[i].id] = entities[i];
        }
    } else {
        this.entities = entities || {};
    }

    this.transients = {};

    if (iD.debug) {
        Object.freeze(this);
        Object.freeze(this.entities);
    }
};

iD.Graph.prototype = {
    entity: function(id) {
        return this.entities[id];
    },

    transient: function(entity, key, fn) {
        var id = entity.id,
            transients = this.transients[id] || (this.transients[id] = {});

        if (transients[key]) {
            return transients[key];
        }

        return transients[key] = fn.call(entity);
    },

    parentStructure: function(ways) {
        var nodes = {};
        ways.forEach(function(w) {
            _.uniq(w.nodes).forEach(function(n) {
                if (typeof nodes[n.id] === 'undefined') nodes[n.id] = 0;
                nodes[n.id]++;
            });
        });
        return nodes;
    },

    parentWays: function(id) {
        // This is slow and a bad hack.
        return _.filter(this.entities, function(e) {
            return e && e.type === 'way' && e.nodes.indexOf(id) !== -1;
        });
    },

    parentRelations: function(id) {
        // This is slow and a bad hack.
        return _.filter(this.entities, function(e) {
            return e && e.type === 'relation' &&
                _.pluck(e.members, 'id').indexOf(id) !== -1;
        });
    },

    merge: function(graph) {
        var entities = _.clone(this.entities);
        _.defaults(entities, graph.entities);
        return iD.Graph(entities);
    },

    replace: function(entity) {
        var entities = _.clone(this.entities);
        entities[entity.id] = entity;
        return iD.Graph(entities);
    },

    remove: function(entity) {
        var entities = _.clone(this.entities);

        if (entity.created()) {
            delete entities[entity.id];
        } else {
            entities[entity.id] = undefined;
        }

        return iD.Graph(entities);
    },

    // get all objects that intersect an extent.
    intersects: function(extent) {
        var items = [];
        for (var i in this.entities) {
            var entity = this.entities[i];
            if (entity && entity.intersects(extent, this)) {
                items.push(this.fetch(entity.id));
            }
        }
        return items;
    },

    // Resolve the id references in a way, replacing them with actual objects.
    fetch: function(id) {
        var entity = this.entities[id], nodes = [];
        if (!entity || !entity.nodes || !entity.nodes.length) return entity;
        for (var i = 0, l = entity.nodes.length; i < l; i++) {
            nodes[i] = this.fetch(entity.nodes[i]);
        }
        return iD.Entity(entity, {nodes: nodes});
    },

    difference: function (graph) {
        var result = [];

        _.each(this.entities, function(entity, id) {
            if (entity !== graph.entities[id]) {
                result.push(id);
            }
        });

        _.each(graph.entities, function(entity, id) {
            if (entity && !this.entities.hasOwnProperty(id)) {
                result.push(id);
            }
        }, this);

        return result.sort();
    },

    modified: function() {
        var result = [];
        _.each(this.entities, function(entity, id) {
            if (entity && entity.modified()) result.push(id);
        });
        return result;
    },

    created: function() {
        var result = [];
        _.each(this.entities, function(entity, id) {
            if (entity && entity.created()) result.push(id);
        });
        return result;
    },

    deleted: function() {
        var result = [];
        _.each(this.entities, function(entity, id) {
            if (!entity) result.push(id);
        });
        return result;
    }
};
