
var Class = require('better-js-class');
var cps = require('cps');
var $U = require('underscore');

var DB = require('./db.js');

module.exports = function() {
    var Model = {
        OPTIMISTIC_LOCK_EXCEPTION: 'optimistic_lock_exception'
    };

    var Row  = Class({
        _init: function(data, cfg) {
            this._table = cfg.table;
            this._data = data;
        },

        getId: function() {
            return this._data[this._table.getIdFieldName()];
        },

        _getVersion: function() {
            return this._data[this._table.getVersionFieldName()];
        },

        _nextVersion: function() {
            return this._data[this._table.getVersionFieldName()] + 1;
        },

        _updateLocalData: function(dto) {
            for (var k in dto) {
                var v = dto[k];
                this._data[k] = v;
            }
        },

        _refineDtoForUpdate: function(dto) {

            var res = {};

            for (var k in dto) {
                if (this._table._hasUpdatableField(k)) {
                    res[k] = dto[k];
                }
            }

            var d = new Date();
            res[this._table.getUpdatedFieldName()] = d;

            return res;
        },

        updateWithoutOptimisticLock: function(conn, dto, cb) {
            this._update(conn, dto, null, cb);
        },

        update: function(conn, dto, cb) {
            var me = this;

            dto['version'] = this._nextVersion();
            var cond = DB.format('version = ?', [this._getVersion()]);

            cps.seq([
                function(_, cb) {
                    me._update(conn, dto, cond, cb);
                },
                function(res, cb) {
                    if (res.changedRows === 0) {
                        throw new Error(Model.OPTIMISTIC_LOCK_EXCEPTION);
                    } else {
                        cb(null, res);
                    }
                }
            ], cb);
        },

        _update: function(conn, dto, conditions, cb) {
            var me = this;

            // dto[this._table.getUpdatedFieldName()] = new Date();
            dto = this._refineDtoForUpdate(dto);

            var l = [
                ' update ', this._table.getName(), ' set '
            ];

            var first = true;
            for (var k in dto) {
                var v = dto[k];
                if (first) {
                    first = false;
                } else {
                    l.push(', ');
                }
                l.push(
                    ' ', k, ' = ', conn.escape(v)
                );
            }

            l.push(
                ' where ', this._table.getIdFieldName(), ' = ', this.getId()
            );

            l.push(
                ' and ', conditions
            );

            l.push(' ; ');

            var q = l.join('');

            // console.log(q);

            cps.seq([
                function(_, cb) {
                    conn.query(q, cb);
                },
                function(res, cb) {
                    me._updateLocalData(dto);
                    cb(null, res);
                }
            ], cb);
        },

        get: function(fieldName) {
            return this._data[fieldName]
        },

        _load: function(conn, name, cb) {
            var cfg = this._table._lookupLinksToMap(name);
            if (cfg) {
                return this.linksTo(conn, name, cb);
            }

            var cfg = this._table._lookupLinkedByMap(name);
            if (cfg) {
                return this.linkedBy(conn, name, cb);
            }

            var cfg = this._table._lookupRelatesToMap(name);
            if (cfg) {
                return this.relatesTo(conn, name, cb);
            }

            throw new Error('no linksTo or linkedBy or relatesTo: ' + name + ' defined on: ' + this._table.getName());
        },

        load: function(conn, name, cb) {
            var value = this.get(name);
            if (value) {
                cb(null, value);
            } else {
                this._load(conn, name, cb);
            }
        },

        linksTo: function(conn, name, cb) {
            var me = this;

            var cfg = this._table._lookupLinksToMap(name);

            if (cfg) {
                var otherTable = this._table._db.get(cfg.table).Table;
                return cps.seq([
                    function(_, cb) {
                        otherTable.findById(conn, me.get(cfg.key), cb);
                    },
                    function(res, cb) {
                        me._data[cfg.name] = res;
                        cb(null, res);
                    }
                ], cb);
            } else {
                throw new Error('no linksTo: ' + name + ' defined on: ' + this._table.getName());
            }
        },

        linkedBy: function(conn, name, cb) {
            var me = this;

            var cfg = this._table._lookupLinkedByMap(name);

            if (cfg) {
                var otherTable = this._table._db.get(cfg.table).Table;
                return cps.seq([
                    function(_, cb) {
                        otherTable.find(conn, otherTable.baseQuery('where ' + cfg.key + ' = ?', [me.getId()]), cb);
                    },
                    function(res, cb) {
                        me._data[cfg.name] = res;
                        cb(null, me._data[cfg.name]);
                    }
                ], cb);
            } else {
                throw new Error('no linkedBy: ' + name + ' defined on: ' + this._table.getName());
            }
        },

        relatesTo: function(conn, name, cb) {
            var me = this;

            var cfg = this._table._lookupRelatesToMap(name);
            if (cfg) {
                var otherTable = this._table._db.get(cfg.table).Table;
                var throughTable = this._table._db.get(cfg.through).Table;

                return cps.seq([
                    function(_, cb) {
                        var _q = 'select t.* from ' +
                                otherTable.getName()  + ' t, ' +
                                throughTable.getName() + ' r where ' +
                                'r.' + cfg['leftKey'] + ' = ? and ' +
                                'r.' + cfg['rightKey'] + ' = t.' + otherTable.getIdFieldName()
                            ;
                        var q = DB.format(_q, [me.getId()]);
                        otherTable.find(conn, q, cb);
                    },
                    function(res, cb) {
                        me._data[cfg.name] = res;
                        cb(null, me._data[cfg.name]);
                    }
                ], cb);
            } else {
                throw new Error('no relatesTo: ' + name + ' defined on: ' + this._table.getName());
            }
        }
    });

    var Table = Class({
        _init: function(cfg) {
            this._name = cfg.name;
            this._idFieldName = cfg.idFieldName || 'id';
            this._versionFieldName = cfg.versionFieldName || 'version';
            this._createdFieldName = cfg.createdFieldName || 'date_created';
            this._updatedFieldName = cfg.updatedFieldName || 'last_updated';

            this._rowClass = cfg['rowClass'];
            // this._schema = cfg['schema'];
            this._db = cfg['db'];

            this._linksToMap = {};
            this._linkedByMap = {};
            this._relatesToMap = {};
        },

        getName: function() {
            return this._name;
        },

        getIdFieldName: function() {
            return this._idFieldName;
        },

        getVersionFieldName: function() {
            return this._versionFieldName;
        },

        getCreatedFieldName: function() {
            return this._createdFieldName;
        },

        getUpdatedFieldName: function() {
            return this._updatedFieldName;
        },

        _hasUpdatableField: function(field) {
            if (field == this.getIdFieldName()) {
                return false;
            }
            if (field == this.getCreatedFieldName()) {
                return false;
            }
            var schema = this._db._schema[this.getName()];
            return $U.contains(schema, field);
        },

        _refineDto: function(dto, autoId) {
            autoId = (autoId === undefined)? true: autoId;
            var res = {};

            var schema = this._db._schema[this.getName()];
            $U.each(schema, function(field) {
                res[field] = dto[field];
            });

            if (autoId) {
                delete res[this.getIdFieldName()];
            }

            return res;
        },

        _refineDtoForCreate: function(dto) {
            var res = this._refineDto(dto);

            var d = new Date();
            res[this.getCreatedFieldName()] = d;
            res[this.getUpdatedFieldName()] = d;
            res[this.getVersionFieldName()] = 0;

            return res;
        },

        create: function(conn, dto, cb) {
            dto = this._refineDtoForCreate(dto);
            this._create(conn, dto, cb);
        },

        _create: function(conn, dto, cb) {
            var me = this;

            var l = [
                ' insert into ' + this.getName() + ' set '
            ];

            var first = true;

            $U.each(dto, function(v, k) {
                if (first) {
                    first = false;
                } else {
                    l.push(', ');
                }
                l.push(
                    ' ', k, ' = ', conn.escape(v)
                );
            });

            l.push(' ; ');

            var q = l.join('');

            // console.log(q);

            cps.seq([
                function(_, cb) {
                    conn.query(q, cb);
                },
                function(res, cb) {
                    dto[me._idFieldName] = res.insertId;
                    cb(null, new me._rowClass(dto));
                }
            ], cb);
        },

        clone: function(conn, dto, cb) {
            dto = this._refineDto(dto, false);
            this._create(conn, dto, cb);
        },

        baseQuery: function(str, bindings) {
            var q = ' select * from ' + this.getName() + ' ';
            var further;

            if (str != null) {
                if (bindings == null) {
                    further = str;
                } else {
                    further = DB.format(str, bindings);
                }
            }

            if (further != null) {
                q += further;
            }
            return q;
        },

        findById: function(conn, id, cb) {
            var me = this;

            cps.seq([
                function(_, cb) {
                    me.find(conn, me.baseQuery('where ' + me.getIdFieldName() + ' = ? ', [id]), cb);
                },
                function(res) {
                    cb(null, res[0])
                }
            ], cb);
        },

        lockById: function(conn, id, cb) {
            var me = this;

            cps.seq([
                function(_, cb) {
                    me.find(conn, me.baseQuery('where ' + me.getIdFieldName() + ' = ? for update', [id]), cb);
                },
                function(res) {
                    cb(null, res[0])
                }
            ], cb);
        },

        findAll: function(conn, cb) {
            this.find(conn, this.baseQuery(), cb);
        },

        find: function(conn, q, cb) {
            var me = this;

            cps.seq([
                function(_, cb) {
                    conn.query(q, cb);
                },
                function(res, cb) {
                    cb(null, $U.map(res, function(o) {
                        return new me._rowClass(o);
                    }));
                }
            ], cb);
        },

        linksTo: function(cfg) {
            this._linksToMap[cfg.name] = {
                table: cfg.table,
                key: cfg.key
            };
            return this;
        },

        linkedBy: function(cfg) {
            this._linkedByMap[cfg.name] = {
                table: cfg.table,
                key: cfg.key
            };
            return this;
        },

        relatesTo: function(cfg) {
            this._relatesToMap[cfg.name] = {
                table: cfg.table,
                through: cfg.through,
                leftKey: cfg.leftKey,
                rightKey: cfg.rightKey
            };
            return this;
        },

        _lookupLinksToMap: function(name) {
            var cfg = this._linksToMap[name];
            if (cfg) {
                cfg.name = name;
            }
            return cfg;
        },

        _lookupLinkedByMap: function(name) {
            var cfg = this._linkedByMap[name];
            if (cfg) {
                cfg.name = name;
            }
            return cfg;
        },

        _lookupRelatesToMap: function(name) {
            var cfg = this._relatesToMap[name];
            if (cfg) {
                cfg.name = name;
            }
            return cfg;
        },

        findFirst: function(conn, q, cb) {
            var me = this;

            cps.seq([
                function(_, cb) {
                    me.find(conn, q, cb);
                },
                function(res, cb) {
                    cb(null, res[0]);
                }
            ], cb);
        }
    });

    $U.extend(Model, {
        Row: Row,
        Table: Table
    });

    return Model;
}();
