"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
var extend1 = function (child, parent) { for (var key in parent) {
    if (hasProp.call(parent, key))
        child[key] = parent[key];
} function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, hasProp = {}.hasOwnProperty, slice = [].slice;
var Backbone = require("./backbone");
var logging_1 = require("./logging");
var property_mixins = require("./property_mixins");
var refs = require("./util/refs");
var p = require("./properties");
var string_1 = require("./util/string");
var array_1 = require("./util/array");
var object_1 = require("./util/object");
var types_1 = require("./util/types");
exports.HasProps = (function (superClass) {
    extend1(HasProps, superClass);
    HasProps.prototype.props = {};
    HasProps.prototype.mixins = [];
    HasProps.define = function (object) {
        var name, prop, results;
        results = [];
        for (name in object) {
            prop = object[name];
            results.push((function (_this) {
                return function (name, prop) {
                    var default_value, internal, props, refined_prop, type;
                    if (_this.prototype.props[name] != null) {
                        throw new Error("attempted to redefine property '" + _this.name + "." + name + "'");
                    }
                    if (_this.prototype[name] != null) {
                        throw new Error("attempted to redefine attribute '" + _this.name + "." + name + "'");
                    }
                    Object.defineProperty(_this.prototype, name, {
                        get: function () {
                            var value;
                            value = this.getv(name);
                            return value;
                        },
                        set: function (value) {
                            this.setv(name, value);
                            return this;
                        }
                    }, {
                        configurable: false,
                        enumerable: true
                    });
                    type = prop[0], default_value = prop[1], internal = prop[2];
                    refined_prop = {
                        type: type,
                        default_value: default_value,
                        internal: internal != null ? internal : false
                    };
                    props = object_1.clone(_this.prototype.props);
                    props[name] = refined_prop;
                    return _this.prototype.props = props;
                };
            })(this)(name, prop));
        }
        return results;
    };
    HasProps.internal = function (object) {
        var _object, fn, name, prop;
        _object = {};
        fn = (function (_this) {
            return function (name, prop) {
                var default_value, type;
                type = prop[0], default_value = prop[1];
                return _object[name] = [type, default_value, true];
            };
        })(this);
        for (name in object) {
            prop = object[name];
            fn(name, prop);
        }
        return this.define(_object);
    };
    HasProps.mixin = function () {
        var mixins, names;
        names = 1 <= arguments.length ? slice.call(arguments, 0) : [];
        this.define(property_mixins.create(names));
        mixins = this.prototype.mixins.concat(names);
        return this.prototype.mixins = mixins;
    };
    HasProps.mixins = function (names) {
        return this.mixin.apply(this, names);
    };
    HasProps.override = function (name_or_object, default_value) {
        var name, object, results;
        if (types_1.isString(name_or_object)) {
            object = {};
            object[name] = default_value;
        }
        else {
            object = name_or_object;
        }
        results = [];
        for (name in object) {
            default_value = object[name];
            results.push((function (_this) {
                return function (name, default_value) {
                    var props, value;
                    value = _this.prototype.props[name];
                    if (value == null) {
                        throw new Error("attempted to override nonexistent '" + _this.name + "." + name + "'");
                    }
                    props = object_1.clone(_this.prototype.props);
                    props[name] = object_1.extend({}, value, {
                        default_value: default_value
                    });
                    return _this.prototype.props = props;
                };
            })(this)(name, default_value));
        }
        return results;
    };
    HasProps.define({
        id: [p.Any]
    });
    HasProps.prototype.toString = function () {
        return this.type + "(" + this.id + ")";
    };
    function HasProps(attributes, options) {
        var attrs, default_value, name, ref, ref1, type;
        this.document = null;
        attrs = attributes || {};
        if (!options) {
            options = {};
        }
        this.attributes = {};
        this.properties = {};
        ref = this.props;
        for (name in ref) {
            ref1 = ref[name], type = ref1.type, default_value = ref1.default_value;
            if (type == null) {
                throw new Error("undefined property type for " + this.type + "." + name);
            }
            this.properties[name] = new type({
                obj: this,
                attr: name,
                default_value: default_value
            });
        }
        this._set_after_defaults = {};
        this.setv(attrs, options);
        this.changed = {};
        this._computed = {};
        if (attrs.id == null) {
            this.id = string_1.uniqueId(this.type);
        }
        if (!options.defer_initialization) {
            this.initialize.apply(this, arguments);
        }
    }
    HasProps.prototype.initialize = function (options) {
        var name, prop, ref, results;
        ref = this.properties;
        results = [];
        for (name in ref) {
            prop = ref[name];
            prop.update();
            if (prop.spec.transform) {
                results.push(this.listenTo(prop.spec.transform, "change", function () {
                    return this.trigger('transformchange', this);
                }));
            }
            else {
                results.push(void 0);
            }
        }
        return results;
    };
    HasProps.prototype.setv = function (key, value, options) {
        var attrs, old, prop_name, results, val;
        if (types_1.isObject(key) || key === null) {
            attrs = key;
            options = value;
        }
        else {
            attrs = {};
            attrs[key] = value;
        }
        for (key in attrs) {
            if (!hasProp.call(attrs, key))
                continue;
            val = attrs[key];
            prop_name = key;
            if (this.props[prop_name] == null) {
                throw new Error("property " + this.type + "." + prop_name + " wasn't declared");
            }
            if (!((options != null) && options.defaults)) {
                this._set_after_defaults[key] = true;
            }
        }
        if (!object_1.isEmpty(attrs)) {
            old = {};
            for (key in attrs) {
                value = attrs[key];
                old[key] = this.getv(key);
            }
            HasProps.__super__.setv.call(this, attrs, options);
            if ((options != null ? options.silent : void 0) == null) {
                results = [];
                for (key in attrs) {
                    value = attrs[key];
                    results.push(this._tell_document_about_change(key, old[key], this.getv(key), options));
                }
                return results;
            }
        }
    };
    HasProps.prototype.add_dependencies = function (prop_name, object, fields) {
        var fld, j, len, prop_spec, results;
        if (!types_1.isArray(fields)) {
            fields = [fields];
        }
        prop_spec = this._computed[prop_name];
        prop_spec.dependencies = prop_spec.dependencies.concat({
            obj: object,
            fields: fields
        });
        results = [];
        for (j = 0, len = fields.length; j < len; j++) {
            fld = fields[j];
            results.push(this.listenTo(object, "change:" + fld, prop_spec['callbacks']['changedep']));
        }
        return results;
    };
    HasProps.prototype.define_computed_property = function (prop_name, getter, use_cache) {
        var changedep, prop_spec, propchange;
        if (use_cache == null) {
            use_cache = true;
        }
        if (this.props[prop_name] != null) {
            console.log("attempted to redefine existing property " + this.type + "." + prop_name);
        }
        if (this._computed[prop_name] != null) {
            throw new Error("attempted to redefine existing computed property " + this.type + "." + prop_name);
        }
        changedep = (function (_this) {
            return function () {
                return _this.trigger('changedep:' + prop_name);
            };
        })(this);
        propchange = (function (_this) {
            return function () {
                var firechange, new_val, old_val;
                firechange = true;
                if (prop_spec['use_cache']) {
                    old_val = prop_spec.cache;
                    prop_spec.cache = void 0;
                    new_val = _this._get_computed(prop_name);
                    firechange = new_val !== old_val;
                }
                if (firechange) {
                    _this.trigger('change:' + prop_name, _this, _this._get_computed(prop_name));
                    return _this.trigger('change', _this);
                }
            };
        })(this);
        prop_spec = {
            'getter': getter,
            'dependencies': [],
            'use_cache': use_cache,
            'callbacks': {
                changedep: changedep,
                propchange: propchange
            }
        };
        this._computed[prop_name] = prop_spec;
        this.listenTo(this, "changedep:" + prop_name, prop_spec['callbacks']['propchange']);
        return prop_spec;
    };
    HasProps.prototype.set = function (key, value, options) {
        logging_1.logger.warn("HasProps.set('prop_name', value) is deprecated, use HasProps.prop_name = value instead");
        return this.setv(key, value, options);
    };
    HasProps.prototype.get = function (prop_name) {
        logging_1.logger.warn("HasProps.get('prop_name') is deprecated, use HasProps.prop_name instead");
        return this.getv(prop_name);
    };
    HasProps.prototype.getv = function (prop_name) {
        if (this.props[prop_name] == null) {
            throw new Error("property " + this.type + "." + prop_name + " wasn't declared");
        }
        else {
            return HasProps.__super__.getv.call(this, prop_name);
        }
    };
    HasProps.prototype._get_computed = function (prop_name) {
        var computed, getter, prop_spec;
        prop_spec = this._computed[prop_name];
        if (prop_spec == null) {
            throw new Error("computed property " + this.type + "." + prop_name + " wasn't declared");
        }
        if (prop_spec.use_cache && prop_spec.cache) {
            return prop_spec.cache;
        }
        else {
            getter = prop_spec.getter;
            computed = getter.apply(this, [prop_name]);
            if (prop_spec.use_cache) {
                prop_spec.cache = computed;
            }
            return computed;
        }
    };
    HasProps.prototype.ref = function () {
        return refs.create_ref(this);
    };
    HasProps.prototype.set_subtype = function (subtype) {
        return this._subtype = subtype;
    };
    HasProps.prototype.attribute_is_serializable = function (attr) {
        var prop;
        prop = this.props[attr];
        if (prop == null) {
            throw new Error(this.type + ".attribute_is_serializable('" + attr + "'): " + attr + " wasn't declared");
        }
        else {
            return !prop.internal;
        }
    };
    HasProps.prototype.serializable_attributes = function () {
        var attrs, name, ref, value;
        attrs = {};
        ref = this.attributes;
        for (name in ref) {
            value = ref[name];
            if (this.attribute_is_serializable(name)) {
                attrs[name] = value;
            }
        }
        return attrs;
    };
    HasProps._value_to_json = function (key, value, optional_parent_object) {
        var i, j, len, ref_array, ref_obj, subkey, v;
        if (value instanceof HasProps) {
            return value.ref();
        }
        else if (types_1.isArray(value)) {
            ref_array = [];
            for (i = j = 0, len = value.length; j < len; i = ++j) {
                v = value[i];
                ref_array.push(HasProps._value_to_json(i, v, value));
            }
            return ref_array;
        }
        else if (types_1.isObject(value)) {
            ref_obj = {};
            for (subkey in value) {
                if (!hasProp.call(value, subkey))
                    continue;
                ref_obj[subkey] = HasProps._value_to_json(subkey, value[subkey], value);
            }
            return ref_obj;
        }
        else {
            return value;
        }
    };
    HasProps.prototype.attributes_as_json = function (include_defaults, value_to_json) {
        var attrs, key, ref, value;
        if (include_defaults == null) {
            include_defaults = true;
        }
        if (value_to_json == null) {
            value_to_json = HasProps._value_to_json;
        }
        attrs = {};
        ref = this.serializable_attributes();
        for (key in ref) {
            if (!hasProp.call(ref, key))
                continue;
            value = ref[key];
            if (include_defaults) {
                attrs[key] = value;
            }
            else if (key in this._set_after_defaults) {
                attrs[key] = value;
            }
        }
        return value_to_json("attributes", attrs, this);
    };
    HasProps._json_record_references = function (doc, v, result, recurse) {
        var elem, j, k, len, model, results, results1;
        if (v == null) {
        }
        else if (refs.is_ref(v)) {
            if (!(v.id in result)) {
                model = doc.get_model_by_id(v.id);
                return HasProps._value_record_references(model, result, recurse);
            }
        }
        else if (types_1.isArray(v)) {
            results = [];
            for (j = 0, len = v.length; j < len; j++) {
                elem = v[j];
                results.push(HasProps._json_record_references(doc, elem, result, recurse));
            }
            return results;
        }
        else if (types_1.isObject(v)) {
            results1 = [];
            for (k in v) {
                if (!hasProp.call(v, k))
                    continue;
                elem = v[k];
                results1.push(HasProps._json_record_references(doc, elem, result, recurse));
            }
            return results1;
        }
    };
    HasProps._value_record_references = function (v, result, recurse) {
        var elem, immediate, j, k, l, len, len1, obj, results, results1, results2;
        if (v == null) {
        }
        else if (v instanceof HasProps) {
            if (!(v.id in result)) {
                result[v.id] = v;
                if (recurse) {
                    immediate = v._immediate_references();
                    results = [];
                    for (j = 0, len = immediate.length; j < len; j++) {
                        obj = immediate[j];
                        results.push(HasProps._value_record_references(obj, result, true));
                    }
                    return results;
                }
            }
        }
        else if (v.buffer instanceof ArrayBuffer) {
        }
        else if (types_1.isArray(v)) {
            results1 = [];
            for (l = 0, len1 = v.length; l < len1; l++) {
                elem = v[l];
                results1.push(HasProps._value_record_references(elem, result, recurse));
            }
            return results1;
        }
        else if (types_1.isObject(v)) {
            results2 = [];
            for (k in v) {
                if (!hasProp.call(v, k))
                    continue;
                elem = v[k];
                results2.push(HasProps._value_record_references(elem, result, recurse));
            }
            return results2;
        }
    };
    HasProps.prototype._immediate_references = function () {
        var attrs, key, result, value;
        result = {};
        attrs = this.serializable_attributes();
        for (key in attrs) {
            value = attrs[key];
            HasProps._value_record_references(value, result, false);
        }
        return object_1.values(result);
    };
    HasProps.prototype.references = function () {
        var references;
        references = {};
        HasProps._value_record_references(this, references, true);
        return object_1.values(references);
    };
    HasProps.prototype.attach_document = function (doc) {
        if (this.document !== null && this.document !== doc) {
            throw new Error("models must be owned by only a single document");
        }
        this.document = doc;
        if (this._doc_attached != null) {
            return this._doc_attached();
        }
    };
    HasProps.prototype.detach_document = function () {
        return this.document = null;
    };
    HasProps.prototype._tell_document_about_change = function (attr, old, new_, options) {
        var need_invalidate, new_id, new_ref, new_refs, old_id, old_ref, old_refs;
        if (!this.attribute_is_serializable(attr)) {
            return;
        }
        if (this.document !== null) {
            new_refs = {};
            HasProps._value_record_references(new_, new_refs, false);
            old_refs = {};
            HasProps._value_record_references(old, old_refs, false);
            need_invalidate = false;
            for (new_id in new_refs) {
                new_ref = new_refs[new_id];
                if (!(new_id in old_refs)) {
                    need_invalidate = true;
                    break;
                }
            }
            if (!need_invalidate) {
                for (old_id in old_refs) {
                    old_ref = old_refs[old_id];
                    if (!(old_id in new_refs)) {
                        need_invalidate = true;
                        break;
                    }
                }
            }
            if (need_invalidate) {
                this.document._invalidate_all_models();
            }
            return this.document._notify_change(this, attr, old, new_, options);
        }
    };
    HasProps.prototype.materialize_dataspecs = function (source) {
        var data, name, prop, ref;
        data = {};
        ref = this.properties;
        for (name in ref) {
            prop = ref[name];
            if (!prop.dataspec) {
                continue;
            }
            if ((prop.optional || false) && prop.spec.value === null && (!(name in this._set_after_defaults))) {
                continue;
            }
            data["_" + name] = prop.array(source);
            if ((prop.spec.field != null) && prop.spec.field in source._shapes) {
                data["_" + name + "_shape"] = source._shapes[prop.spec.field];
            }
            if (prop instanceof p.Distance) {
                data["max_" + name] = array_1.max(data["_" + name]);
            }
        }
        return data;
    };
    return HasProps;
})(Backbone.Model);
