define(
    ['utils'],
	function (utils) {
        "use strict";

        var plugins = {};

        var create = function(cfg, data, row, col) {
            row = (typeof row == 'number') ? row : cfg.rows.num - 1;
            col = (typeof col == 'number') ? col : cfg.cols.num - 1;
            data = (typeof data != 'undefined') ? data : cfg.content;

            data = normaliseData(cfg, data);
            resolveMergedCells(cfg);

            if (Array.isArray(data)) {
                if (Array.isArray(data[0])) {
                    return createTable(cfg, data);
                }
                else {
                    return createRow(cfg, data, row);
                }
            }

            return (data === null) ? data : createCell(cfg, data, row, col);
        };

        var createTable = function(cfg, data) {
            var table = document.createElement(cfg.pseudo ? 'div' : 'table');
            var body = document.createElement(cfg.pseudo ? 'div' : 'tbody');
            var classes = Array.isArray(cfg.classes) ? cfg.classes : [];
            var caption;

            data.forEach(function(data, i) {
                body.appendChild(createRow(cfg, data, i));
            });

            if (typeof cfg.caption == 'string' && cfg.caption !== '') {
                caption = document.createElement(cfg.pseudo ? 'div' : 'caption');
                caption.className = 'table-caption';
                caption.insertAdjacentHTML('beforeend', cfg.caption);
                table.appendChild(caption);
            }

            body.className = 'table-body';
            classes.push('table');
            table.className = classes.join(' ');
            table.style.tableLayout = cfg.layout || 'fixed';
            table.appendChild(body);

            if (!(cfg instanceof Table)) {
                table.style.width = cfg.width + 'px' || 'auto';
            }

            return table;
        };

        var createRow = function(cfg, data, row) {
            var tr = document.createElement(cfg.pseudo ? 'div' : 'tr');

            tr.className = 'table-row';

            data.forEach(function(data, i) {
                if (data === null) {
                    return data;
                }

                tr.appendChild(createCell(cfg, data, row, i));
            });

            return tr;
        };

        var createCell = function(cfg, data, row, col) {
            var type = (cfg.cols.headers.indexOf(row) < 0 && cfg.rows.headers.indexOf(col) < 0) ? 'td' : 'th';
            var cell = document.createElement(cfg.pseudo ? 'div' : type);
            var classes = Array.isArray(data.classes) ? data.classes : [];

            if (data.rowspan) {
                cell.rowSpan = data.rowspan;
            }
            if (data.colspan) {
                cell.colSpan = data.colspan;
            }

            //sort classes
            if (cfg.rows.class) {
                classes.push('row' + row);
            }
            if (Array.isArray(cfg.rows.classes) && cfg.rows.classes[row]) {
                classes.push(cfg.rows.classes[row]);
            }
            if (cfg.cols.class) {
                classes.push('col' + col);
            }
            if (Array.isArray(cfg.cols.classes) && cfg.cols.classes[col]) {
                classes.push(cfg.cols.classes[col]);
            }
            if (!data.html && !data.el && cfg.placeholder) {
                classes.push('table-cell-placeholder');
            }

            //set dimensions
            if (typeof cfg.rows.height == 'number' && cfg.cols.headers.indexOf(row) < 0) {
                cell.style.height = cfg.rows.height + 'px';
            }
            if (typeof cfg.cols.width == 'number' && cfg.rows.headers.indexOf(col) < 0) {
                cell.style.width = cfg.cols.width + 'px';
            }

            if (typeof data.html == 'number') {
                data.html = data.html.toString();
            }
            if (data.el) {
                cell.appendChild(data.el);
            }

            classes.push('table-cell');
            classes.push('table-' + type);
            cell.className = classes.join(' ');

            cell.setAttribute('data-row', row.toString());
            cell.setAttribute('data-col', col.toString());
            cell.insertAdjacentHTML('beforeend', (data.html || cfg.placeholder || ''));

            return cell;
        };

        var normaliseData = function(cfg, data) {
            if (!Array.isArray(cfg.rows.headers)) {
                cfg.rows.headers = (cfg.rows.headers === true) ? [0] : [];
            }
            if (!Array.isArray(cfg.cols.headers)) {
                cfg.cols.headers = (cfg.cols.headers === true) ? [0] : [];
            }

            if (Array.isArray(data)) {
                if (Array.isArray(data[0])) {
                    return normaliseContentData(cfg, data);
                }
                else {
                    return normaliseRowData(cfg, data);
                }
            }

            return normaliseCellData(cfg, data);
        };

        var normaliseContentData = function(cfg, data) {
            var i;

            for (i = 0; i < cfg.rows.num; i++) {
                data[i] = Array.isArray(data[i]) ? data[i] : [];
                normaliseRowData(cfg, data[i]);
            }

            return data;
        };

        var normaliseRowData = function(cfg, data) {
            var i;

            for (i = 0; i < cfg.cols.num; i++) {
                data[i] = normaliseCellData(cfg, data[i]);
            }

            return data;
        };

        var normaliseCellData = function(cfg, data) {
            if (data === null) {
                return data;
            }

            if (utils.isObject(data)) {
                if (data instanceof Element) {
                    data = {
                        el: data
                    };
                }
            }
            else {
                data = {
                    html: data
                };
            }

            //clean these up for use later
            if (typeof data.colspan != 'number' || data.colspan === 0) {
                data.colspan = 1;
            }
            if (typeof data.rowspan != 'number' || data.rowspan === 0) {
                data.rowspan = 1;
            }

            return data;
        };

        //TODO deal with merges that are outside of the table, i.e. no row/col for the span to go - data attributes??
        var resolveMergedCells = function(cfg) {
            var i, span, array;

            //TODO find a 'nicer' way, i.e. without removing existing values
            //clear null values and start again
            removeValueFromArray(cfg.content, null);

            //resolve all colspans first so they aren't screwed up when resolving rowspans
            cfg.content.forEach(function(data, row) {
                data.forEach(function(data, col) {
                    if (data === null) {
                        return;
                    }

                    if (data.colspan > 1) {
                        array = utils.getPopulatedArray(data.colspan - 1, null);
                        utils.spliceArray(cfg.content[row], col + 1, array);
                    }
                });
            });

            cfg.content.forEach(function(data, row) {
                data.forEach(function(data, col) {
                    if (data === null) {
                        return;
                    }

                    if (data.rowspan > 1) {
                        for (i = row + 1, span = i + data.rowspan - 1; i < span; i++) {
                            if (typeof cfg.content[i] != 'undefined') {
                                array = utils.getPopulatedArray(data.colspan, null);
                                utils.spliceArray(cfg.content[i], col, array);
                            }
                        }
                    }
                });
            });

            removeExtraneousData(cfg, cfg.content);
        };

        var removeExtraneousData = function(cfg, data) {
            if (data.length > cfg.rows.num) {
                data.splice(cfg.rows.num, data.length - cfg.rows.num);
            }

            data.forEach(function(data) {
                if (data.length > cfg.cols.num) {
                    data.splice(cfg.cols.num, data.length - cfg.cols.num);
                }
            });
        };

        var removeValueFromArray = function(array, val) {
            for (var i = 0; i < array.length; i++) {
                if (Array.isArray(array[i])) {
                    removeValueFromArray(array[i], val);
                }
                else {
                    if (array[i] === val) {
                        array.splice(i, 1);
                        i--;
                    }
                }
            }

            return array;
        };

        var construct = function(cfg) {
            return new Table(cfg);
        };

        function Table(cfg) {
            //keep count for saving data where multiple instances
            Table.count = ++Table.count || 0;
            console.log('Table count: ' + Table.count);

            this.pseudo = false;
            this.events = [];
            this.index = Table.count;
            this.id = 'table' + this.index;
            this.parent = null;
            this.placeholder = ''; //content for empty cells (better name?!)
            this.container = utils.createElement('div', 'table-container');
            this.el = null;
            this.caption = null;
            this.body = null;
            this.foot = null;
            this.width = 512;
            this.height = 'auto';
            this.layout = 'fixed';
            this.rows = {
                num: 0,
                initial: 0,
                headers: false,
                class: true, //.row0 etc.
                height: 'auto',
                dom: []
            };
            this.cols = {
                num: 0,
                initial: 0,
                headers: false,
                class: true, //.col0 etc.
                width: 'auto',
                dom: []
            };
            this.classes = [];
            this.content = [];
            this.plugins = [];
            this.loaded = false;

            utils.extendDeep(this, cfg);
            this.load();
        }

        Table.prototype = {
            modules: {}
        };

        Table.prototype.init = function() {
            this.createTable();
            this.setWidth();
            this.storeInitialValues();
            this.initButtonEvents();
            this.initPlugins();
        };

        Table.prototype.load = function() {
            //TODO fire event when loaded, including images
            if (Array.isArray(this.images) && this.images.length) {
                //load images and pass init as callback. fire table-loaded event.
            }
            else {
                this.init();
            }
        };

        /**
         * store any initial values for reference later
         */
        Table.prototype.storeInitialValues = function() {
            this.rows.initial = this.rows.num;
            this.cols.initial = this.cols.num;
        };

        /**
         * create dom elements and store any refs to them
         */
        Table.prototype.createTable = function() {
            var tmp;

            //create/store necessary elements and containers
            this.el = create(this);

            this.body = this.el.getElementsByClassName('table-body')[0];
            this.container.appendChild(this.el);

            this.parent = this.parent ? document.querySelector(this.parent) : document.getElementsByTagName('body')[0];
            this.parent.appendChild(this.container);

            //store row and cells (in cols.dom including null where merged) elements
            this.rows.dom = utils.toArray(this.body.childNodes);
            this.content.forEach(function(data, row) {
                tmp = utils.toArray(this.rows.dom[row].childNodes);

                data.forEach(function(data, col) {
                    this.cols.dom[col] = this.cols.dom[col] || [];
                    this.cols.dom[col].push((data === null) ? null : tmp.shift());
                }.bind(this));
            }.bind(this));
        };

        /**
         * set table (container) width
         */
        Table.prototype.setWidth = function() {
            //col width takes precedence over table width
            if (typeof this.cols.width != 'number' && typeof this.width == 'number') {
                this.container.style.width = this.width + 'px';
                this.el.style.width = '100%';
            }
        };

        /**
         * loop enabled buttons and add event listeners to container
         */
        Table.prototype.initButtonEvents = function() {
            if (this.buttons && Array.isArray(this.buttons.enabled)) {
                this.buttons.enabled.forEach(function(name) {
                    this.on(name);
                }.bind(this));
            }
        };

        /**
         * initialise enabled plugins and add event listeners to container
         */
        Table.prototype.initPlugins = function() {
            var plugin;

            this.plugins.forEach(function(name) {
                if (plugins[name]) {
                    plugin = plugins[name].construct();
                    plugin.table = this;
                    utils.extendDeep(plugin, this[name]);

                    if (Array.isArray(plugin.events)) {
                        plugin.events.forEach(function(e) {
                            this.on(e);
                        }.bind(this));
                    }

                    plugin.init();
                    this.plugins[name] = plugin;
                }
                else {
                    throw new Error('Plugin not found: ' + name);
                }
            }.bind(this));
        };

        /**
         * utility function for iterating over content data
         */
        Table.prototype.iterateContent = function(callback, args) {
            this.content.forEach(function(row, i) {
                row.forEach(function(cell, j) {
                    callback.apply(this, [row, cell, i, j].concat(args));
                }.bind(this));
            }.bind(this));
        };

        /**
         * add event listener to container and push event type to event array
         * @param {string} type - event type
         */
        Table.prototype.on = function(type) {
            //check event isn't already there to avoid duplicates
            if (this.events.indexOf(type) < 0) {
                this.events.push(type);
                this.container.addEventListener(type, this);
            }
        };

        /**
         * trigger an event on the container
         * @param {string} type - event type
         * @param {object} detail - an object to be passed in e.detail
         * @param {boolean} bubbles - allow the event to bubble (false unless === true)
         */
        Table.prototype.trigger = function(type, detail, bubbles) {
            if (this.events.indexOf(type) > -1) {
                utils.triggerCustomEvent(this.container, type, detail, (bubbles === true));
            }
        };

        /**
         * handle events for this and all plugins
         * @param {Event} e
         */
        Table.prototype.handleEvent = function(e) {
            var prop = utils.toCamelCase(e.type.replace('table-', ''));

            if (e.target.nodeName.toLowerCase() != 'button') {
                e.stopPropagation();
            }

            //trigger main event if one exists (needed?)
            if (prop in this && typeof this[prop] == 'function') {
                this[prop](e);
            }

            //pass event to plugin.handleEvent
            this.plugins.forEach(function(plugin, i, array) {
                if (plugin in array && typeof array[plugin].handleEvent == 'function') {
                    array[plugin].handleEvent(e);
                }
            });
        };

        /**
         * adds plugin to module
         * @param {string} name - plugin name
         * @param {object} module - plugin module
         */
        var extend = function(name, module) {
            plugins[name] = module;
        };

        return {
            create: create,
            construct: construct,
            extend: extend
        };
    }
);