/*!
 * TableExport.js v5.0.0-rc.8 (https://www.travismclarke.com)
 *
 * Copyright (c) 2017 - Travis Clarke - https://www.travismclarke.com
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at

 * http://www.apache.org/licenses/LICENSE-2.0

 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */


;(function (root, factory) {
    if (typeof define === 'function' && define.amd) {
        // AMD
        define(function (require) {
            var $;
            try {
                $ = require('jquery')
            } catch (e) {
            }
            return factory($, require('blobjs'), require('file-saverjs'), require('xlsx'));
        });
    } else if (typeof exports === 'object' && typeof exports.nodeName !== 'string') {
        // CommonJS
        var $;
        try {
            $ = require('jquery')
        } catch (e) {
        }
        module.exports = factory($, require('blobjs'), require('file-saverjs'), require('xlsx'));
    } else {
        // Browser globals
        root.TableExport = factory(root.jQuery, root.Blob, root.saveAs, root.XLSX);
    }
}(this, function ($, Blob, saveAs, XLSX) {
        'use strict';
        /**
         * TableExport main library constructor
         * @param selectors {jQuery} jQuery selector(s)
         * @param options {Object} TableExport configuration options
         * @constructor
         */
        var TableExport = function (selectors, options) {
            var self = this;

            if (!selectors) return _handleError('"selectors" is required. \nUsage: TableExport(selectors, options)');
            if (!self) return new TableExport(selectors, options);

            /**
             * TableExport configuration options (user-defined w/ default fallback)
             */
            self.settings = _extend({}, self.defaults, options);
            /**
             * Selectors (tables) to apply the library to
             */
            self.selectors = _nodesArray(selectors);

            var settings = self.settings;
            settings.ignoreRows = settings.ignoreRows instanceof Array ? settings.ignoreRows : [settings.ignoreRows];
            settings.ignoreCols = settings.ignoreCols instanceof Array ? settings.ignoreCols : [settings.ignoreCols];
            settings.ignoreCSS = self.ignoreCSS instanceof Array ? self.ignoreCSS : [self.ignoreCSS];
            settings.emptyCSS = self.emptyCSS instanceof Array ? self.emptyCSS : [self.emptyCSS];
            settings.formatValue = self.formatValue.bind(this, settings.trimWhitespace);
            settings.bootstrapSettings = _getBootstrapSettings(settings.bootstrap, self.bootstrapConfig, self.defaultButton);

            var _exportData = {};
            self.getExportData = function () {
                return _exportData;
            };

            self.selectors.forEach(function (el) {
                var context = {};

                context.rows = _nodesArray(el.querySelectorAll('tbody > tr'));
                context.rows = settings.headers ? _nodesArray(el.querySelectorAll('thead > tr')).concat(context.rows) : context.rows;
                context.rows = settings.footers ? context.rows.concat(_nodesArray(el.querySelectorAll('tfoot > tr'))) : context.rows;
                context.thAdj = settings.headers ? el.querySelectorAll('thead > tr').length : 0;
                context.filename = settings.filename === 'id'
                    ? (el.getAttribute('id') ? el.getAttribute('id') : self.defaultFilename)
                    : (settings.filename ? settings.filename : self.defaultFilename);
                context.uuid = _uuid(el);

                /**
                 * Initializes table caption with export buttons
                 * @param exportButton {HTMLButtonElement}
                 */
                context.checkCaption = function (exportButton) {
                    var caption = el.querySelectorAll('caption.' + self.defaultCaptionClass);
                    if (caption.length) {
                        caption[0].appendChild(exportButton);
                    } else {
                        caption = document.createElement('caption');
                        caption.className = settings.bootstrapSettings.bootstrapSpacing + settings.position + ' ' + self.defaultCaptionClass;
                        caption.appendChild(exportButton);
                        el.insertBefore(caption, el.firstChild);
                    }
                };

                context.setExportData = (function () {
                    return function (exporter) {
                        var data = LocalStorage.getInstance().getItem(exporter);
                        var type = exporter.substring(exporter.indexOf('-') + 1);
                        _exportData[context.uuid] = _exportData[context.uuid] || {};
                        _exportData[context.uuid][type] = JSON.parse(data);
                    };
                })();

                context.rcMap = new RowColMap().build(context, settings);

                console.debug(context.rcMap);

                var formatMap = _FORMAT_LIST
                    .reduce(function (acc, cur) {
                        acc[cur] = 0;
                        return acc;
                    }, {});

                settings.formats.forEach(
                    function (key) {
                        if (!_isValidFormat(key)) {
                            return _handleError('"' + key + '" is not a valid format. \nFormats: ' + _FORMAT_LIST.join(', '))
                        } else if (!_hasDependencies(key)) {
                            return _handleError('"' + key + '" requires "js-xlsx".');
                        } else if (!formatMap[key]) {
                            context.setExportData(self.exporters.build.call(self, context, key));
                            formatMap[key]++;
                        }
                    }
                );
            });

            var exportButton = document.querySelectorAll('button[' + self.localStorageKey + ']');
            _on(exportButton, 'click', self.downloadHandler, self);

            return self;
        };

        TableExport.prototype = {
            /**
             * Version.
             * @memberof TableExport.prototype
             */
            version: '5.0.0-rc.8',
            /**
             * Default library options.
             * @memberof TableExport.prototype
             */
            defaults: {
                headers: true,                              // (Boolean), display table headers (th or td elements) in the <thead>, (default: true)
                footers: true,                              // (Boolean), display table footers (th or td elements) in the <tfoot>, (default: false)
                formats: ['xlsx', 'csv', 'txt'],            // (String[]), filetype(s) for the export, (default: ['xlsx', 'csv', 'txt'])
                filename: 'id',                             // (id, String), filename for the downloaded file, (default: 'id')
                bootstrap: false,                           // (Boolean), style buttons using bootstrap, (default: true)
                exportButtons: true,                        // (Boolean), automatically generate the built-in export buttons for each of the specified formats (default: true)
                position: 'bottom',                         // (top, bottom), position of the caption element relative to table, (default: 'bottom')
                ignoreRows: null,                           // (Number, Number[]), row indices to exclude from the exported file(s) (default: null)
                ignoreCols: null,                           // (Number, Number[]), column indices to exclude from the exported file(s) (default: null)
                trimWhitespace: true                        // (Boolean), remove all leading/trailing newlines, spaces, and tabs from cell text in the exported file(s) (default: false)
            },
            /**
             * Constants
             * @memberof TableExport.prototype
             */
            CONSTANTS: {
                FORMAT: {
                    XLSX: 'xlsx',
                    XLSM: 'xlsm',
                    XLSB: 'xlsb',
                    BIFF2: 'biff2',
                    XLS: 'xls',
                    CSV: 'csv',
                    TXT: 'txt'
                },
                TYPE: {
                    STRING: 's',
                    NUMBER: 'n',
                    BOOLEAN: 'b',
                    DATE: 'd'
                }
            },
            /**
             * Character set (character encoding) of the HTML.
             * @memberof TableExport.prototype
             */
            charset: 'charset=utf-8',
            /**
             * Filename fallback for exported files.
             * @memberof TableExport.prototype
             */
            defaultFilename: 'myDownload',
            /**
             * Class applied to each export button element.
             * @memberof TableExport.prototype
             */
            defaultButton: 'button-default',
            /**
             * Class applied to each table caption.
             * @memberof TableExport.prototype
             */
            defaultCaptionClass: 'tableexport-caption',
            /**
             * Namespace (i.e. prefix) applied to each table UUID and LocalStorage key.
             * @memberof TableExport.prototype
             */
            defaultNamespace: 'tableexport-',
            /**
             * Attribute applied to each export button element used to reference a LocalStorage key.
             * @memberof TableExport.prototype
             */
            localStorageKey: 'tableexport-id',
            /**
             * CSS selector or selector[] to exclude/remove cells from the exported file(s).
             * @type {selector|selector[]}
             * @memberof TableExport.prototype
             */
            ignoreCSS: '.tableexport-ignore',
            /**
             * CSS selector or selector[] to replace cells with an empty string in the exported file(s).
             * @type {selector|selector[]}
             * @memberof TableExport.prototype
             */
            emptyCSS: '.tableexport-empty',
            /**
             * Bootstrap configuration classes ['base', 'theme', 'container'].
             * @memberof TableExport.prototype
             */
            bootstrapConfig: ['btn', 'btn-default', 'btn-toolbar'],
            /**
             * Row delimeter
             * @memberof TableExport.prototype
             */
            rowDel: '\r\n',
            /**
             * HTML entity mapping for special characters.
             * @memberof TableExport.prototype
             */
            entityMap: {'&': '&#38;', '<': '&#60;', '>': '&#62;', "'": '&#39;', '/': '&#47;'},
            /**
             * Format configuration
             * @memberof TableExport.prototype
             */
            formatConfig: {
                /**
                 * XLSX (Open XML spreadsheet) file extension configuration
                 * @memberof TableExport.prototype
                 */
                xlsx: {
                    defaultClass: 'xlsx',
                    buttonContent: 'Export to xlsx',
                    mimeType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
                    fileExtension: '.xlsx'
                },
                xlsm: {
                    defaultClass: 'xlsm',
                    buttonContent: 'Export to xlsm',
                    mimeType: 'application/vnd.ms-excel.sheet.macroEnabled.main+xml',
                    fileExtension: '.xlsm'
                },
                xlsb: {
                    defaultClass: 'xlsb',
                    buttonContent: 'Export to xlsb',
                    mimeType: 'application/vnd.ms-excel.sheet.binary.macroEnabled.main',
                    fileExtension: '.xlsb'
                },
                /**
                 * XLS (Binary spreadsheet) file extension configuration
                 * @memberof TableExport.prototype
                 */
                xls: {
                    defaultClass: 'xls',
                    buttonContent: 'Export to xls',
                    separator: '\t',
                    mimeType: 'application/vnd.ms-excel',
                    fileExtension: '.xls',
                    enforceStrictRFC4180: false
                },
                /**
                 * CSV (Comma Separated Values) file extension configuration
                 * @memberof TableExport.prototype
                 */
                csv: {
                    defaultClass: 'csv',
                    buttonContent: 'Export to csv',
                    separator: ',',
                    mimeType: 'text/csv',
                    fileExtension: '.csv',
                    enforceStrictRFC4180: true
                },
                /**
                 * TXT (Plain Text) file extension configuration
                 * @memberof TableExport.prototype
                 */
                txt: {
                    defaultClass: 'txt',
                    buttonContent: 'Export to txt',
                    separator: '  ',
                    mimeType: 'text/plain',
                    fileExtension: '.txt',
                    enforceStrictRFC4180: true
                }
            },
            /**
             * Cell-types override and assertion configuration
             * @memberof TableExport.prototype
             */
            typeConfig: {
                string: {
                    defaultClass: 'tableexport-string'
                },
                number: {
                    defaultClass: 'tableexport-number',
                    assert: function (v) {
                        return !isNaN(v);
                    }
                },
                boolean: {
                    defaultClass: 'tableexport-boolean',
                    assert: function (v) {
                        return v.toLowerCase() === 'true' || v.toLowerCase() === 'false';
                    }
                },
                date: {
                    defaultClass: 'tableexport-date',
                    assert: function (v) {
                        return !(/.*%/.test(v)) && !isNaN(Date.parse(v))
                    }
                }
            },
            exporters: {
                build: function (context, key) {
                    var self = this;
                    var settings = self.settings;
                    var format = self.formatConfig[key];
                    var colDel = format.separator;
                    var rcMap = context.rcMap;

                    var getReturn = function (val) {
                        if (_isEnhanced(key)) {
                            return {
                                v: settings.formatValue(val.textContent),
                                t: self.getType(val.className)
                            }
                        }
                        switch (key) {
                            case _FORMAT.CSV:
                                return '"' + settings.formatValue(val.textContent.replace(/"/g, '""')) + '"';
                            default:
                                return settings.formatValue(val.textContent);
                        }
                    };

                    var dataURI = _nodesArray(context.rows).map(function (val, ir) {
                        if (rcMap.isIgnore(ir)) {
                            return rcMap.handleRowColMapProp(rcMap.TYPE.IGNORE);
                        } else if (rcMap.isEmpty(ir)) {
                            return rcMap.handleRowColMapProp(rcMap.TYPE.EMPTY);
                        }
                        var cols = val.querySelectorAll('th, td');
                        return _nodesArray(cols).map(function (val, ic) {
                            var _return = getReturn(val);
                            if (rcMap.isIgnore(ir, ic)) {
                                return rcMap.handleRowColMapProp(rcMap.TYPE.IGNORE);
                            } else if (rcMap.isEmpty(ir, ic)) {
                                return rcMap.handleRowColMapProp(rcMap.TYPE.EMPTY);
                            } else {
                                return rcMap.handleRowColMapProp(rcMap.TYPE.DEFAULT, ir, ic, key, _return, colDel);
                            }
                        }).processCols(key, colDel);
                    }).processRows(key, self.rowDel);

                    var dataObject = JSON.stringify({
                        data: dataURI,
                        filename: context.filename,
                        mimeType: format.mimeType,
                        fileExtension: format.fileExtension,
                        merges: rcMap.merges
                    });

                    var hashKey = _hashCode({uuid: context.uuid, type: key});

                    settings.exportButtons && context.checkCaption(self.createObjButton(
                        hashKey,
                        dataObject,
                        format.buttonContent,
                        format.defaultClass,
                        settings.bootstrapSettings
                    ));
                    return LocalStorage.getInstance().setItem(hashKey, dataObject, true);
                }
            },
            /**
             * Creates file export buttons
             * @param hashKey {String}
             * @param dataObject {String}
             * @param myContent {String}
             * @param myClass {String}
             * @param bootstrapSettings {Object}
             * @returns Element
             */
            createObjButton: function (hashKey, dataObject, myContent, myClass, bootstrapSettings) {
                var exportButton = document.createElement('button');
                exportButton.setAttribute(this.localStorageKey, hashKey);
                exportButton.className = bootstrapSettings.bootstrapClass + bootstrapSettings.bootstrapTheme + myClass;
                exportButton.textContent = myContent;
                return exportButton
            },
            /**
             * Escapes special characters with HTML entities
             * @memberof TableExport.prototype
             * @param string {String}
             * @returns {String} escaped string
             */
            escapeHtml: function (string) {
                var self = this;
                return String(string).replace(/[&<>'\/]/g, function (s) {
                    return self.entityMap[s];
                });
            },
            /**
             * Unescapes HTML entities to special characters
             * @memberof TableExport.prototype
             * @param string {String}
             * @returns {String} unescaped string
             */
            unescapeHtml: function (string) {
                var str = String(string);
                for (var key in this.entityMap) {
                    str = str.replace(RegExp(this.entityMap[key], 'g'), key);
                }
                return str;
            },
            /**
             * Removes leading/trailing whitespace from cell string
             * @param isTrimWhitespace {Boolean}
             * @param string {String}
             * @returns {String} trimmed string
             */
            formatValue: function (isTrimWhitespace, string) {
                return isTrimWhitespace ? string.trim() : string;
            },
            /**
             * Get cell data-type
             * @param string {String}
             * @returns {String} data-type
             */
            getType: function (string) {
                if (!string) return '';
                var types = this.typeConfig;
                if (~string.indexOf(types.string.defaultClass)) {
                    return _TYPE.STRING;
                } else if (~string.indexOf(types.number.defaultClass)) {
                    return _TYPE.NUMBER;
                } else if (~string.indexOf(types.boolean.defaultClass)) {
                    return _TYPE.BOOLEAN;
                } else if (~string.indexOf(types.date.defaultClass)) {
                    return _TYPE.DATE;
                } else {
                    return '';
                }
            },
            /**
             * Formats datetimes for compatibility with Excel
             * @memberof TableExport.prototype
             * @param v {Number}
             * @param date1904 {Date}
             * @returns {Number} epoch time
             */
            dateNum: function (v, date1904) {
                if (date1904) v += 1462;
                var epoch = Date.parse(v);
                var result = (epoch - new Date(Date.UTC(1899, 11, 30))) / (24 * 60 * 60 * 1000);
                return Math.floor(result);
            },
            /**
             * Creates an Excel spreadsheet from a data string
             * @memberof TableExport.prototype
             * @param data {String}
             * @param merges {Object[]}
             */
            createSheet: function (data, merges) {
                var ws = {};
                var range = {s: {c: 10000000, r: 10000000}, e: {c: 0, r: 0}};
                var types = this.typeConfig;
                for (var R = 0; R !== data.length; ++R) {
                    for (var C = 0; C !== data[R].length; ++C) {
                        if (range.s.r > R) range.s.r = R;
                        if (range.s.c > C) range.s.c = C;
                        if (range.e.r < R) range.e.r = R;
                        if (range.e.c < C) range.e.c = C;
                        var cell = data[R][C];
                        if (!cell || !cell.v) continue;
                        var cell_ref = XLSX.utils.encode_cell({c: C, r: R});

                        if (!cell.t) {
                            if (types.number.assert(cell.v)) cell.t = _TYPE.NUMBER;
                            else if (types.boolean.assert(cell.v)) cell.t = _TYPE.BOOLEAN;
                            else if (types.date.assert(cell.v)) cell.t = _TYPE.DATE;
                            else cell.t = _TYPE.STRING;
                        }
                        if (cell.t === _TYPE.DATE) {
                            cell.t = _TYPE.NUMBER;
                            cell.z = XLSX.SSF._table[14];
                            cell.v = this.dateNum(cell.v);
                        }
                        ws[cell_ref] = cell;
                    }
                }
                // console.debug(merges);
                ws['!merges'] = merges;
                if (range.s.c < 10000000) ws['!ref'] = XLSX.utils.encode_range(range);
                return ws;
            },
            /**
             * Click handler for export button "download"
             * @memberof TableExport.prototype
             */
            downloadHandler: function (event) {
                var target = event.target;
                var object = JSON.parse(LocalStorage.getInstance().getItem(target.getAttribute(this.localStorageKey))),
                    data = object.data,
                    filename = object.filename,
                    mimeType = object.mimeType,
                    fileExtension = object.fileExtension,
                    merges = object.merges;
                this.export2file(data, mimeType, filename, fileExtension, merges);
            },
            /**
             * Excel Workbook constructor
             * @memberof TableExport.prototype
             * @constructor
             */
            Workbook: function () {
                this.SheetNames = [];
                this.Sheets = {};
            },
            /**
             * Converts a string to an arraybuffer
             * @param s {String}
             * @memberof TableExport.prototype
             * @returns {ArrayBuffer}
             */
            string2ArrayBuffer: function (s) {
                var buf = new ArrayBuffer(s.length);
                var view = new Uint8Array(buf);
                for (var i = 0; i !== s.length; ++i) view[i] = s.charCodeAt(i) & 0xFF;
                return buf;
            },
            /**
             * Exports and downloads the file
             * @memberof TableExport.prototype
             * @param data {String}
             * @param mime {String} mime type
             * @param name {String} filename
             * @param extension {String} file extension
             * @param merges {Object[]}
             */
            export2file: function (data, mime, name, extension, merges) {
                data = this.getRawData(data, extension, name, merges);

                if (_isMobile) {
                    // TODO: fix dataURI on iphone (xlsx & xls)
                    var dataURI = 'data:' + mime + ';' + this.charset + ',' + data;
                    this.downloadDataURI(dataURI, name, extension);
                } else {
                    saveAs(new Blob([data],
                        {type: mime + ';' + this.charset}),
                        name + extension, true);
                }
            },
            downloadDataURI: function (dataURI, name, extension) {
                var encodedUri = encodeURI(dataURI);
                var link = document.createElement('a');
                link.setAttribute('href', encodedUri);
                link.setAttribute('download', name + extension);
                document.body.appendChild(link);
                link.click();
            },
            getBookType: function (key) {
                switch (key) {
                    case _FORMAT.XLS:
                        return _FORMAT.BIFF2;
                    default:
                        return key;
                }
            },
            getRawData: function (data, extension, name, merges) {
                var key = extension.substring(1);

                if (_isEnhanced(key)) {
                    var wb = new this.Workbook(),
                        ws = this.createSheet(data, merges),
                        bookType = this.getBookType(key);

                    name = name || '';
                    wb.SheetNames.push(name);
                    wb.Sheets[name] = ws;
                    var wopts = {
                            bookType: bookType,
                            bookSST: false,
                            type: 'binary'
                        },
                        wbout = XLSX.write(wb, wopts);

                    data = this.string2ArrayBuffer(wbout);
                }
                return data;
            },
            getFileSize: function (data, extension) {
                var binary = this.getRawData(data, extension);
                return binary instanceof ArrayBuffer
                    ? binary.byteLength
                    : this.string2ArrayBuffer(binary).byteLength;
            },
            /**
             * Updates the library instance with new/updated options
             * @param options {Object} TableExport configuration options
             * @returns {TableExport} updated TableExport instance
             */
            update: function (options) {
                this.remove();
                return new TableExport(this.selectors, _extend({}, this.defaults, options));
            },
            /**
             * Reset the library instance to its original state
             * @returns {TableExport} original TableExport instance
             */
            reset: function () {
                this.remove();
                return new TableExport(this.selectors, this.settings);
            },
            /**
             * Remove the instance (i.e. caption containing the export buttons)
             */
            remove: function () {
                var self = this;
                this.selectors.forEach(function (el) {
                    var caption = el.querySelector('caption.' + self.defaultCaptionClass);
                    caption && el.removeChild(caption);
                });
            }
        };

        /**
         * LocalStorage main interface constructor
         * @memberof TableExport.prototype
         * @constructor
         */
        var LocalStorage = function () {
            this._instance = null;

            this.store = localStorage;
            this.namespace = TableExport.prototype.defaultNamespace;
            this.getKey = function (key) {
                return this.namespace + key;
            };
            this.setItem = function (_key, value, overwrite) {
                var key = this.getKey(_key);
                if (this.exists(key) && !overwrite) {
                    return;
                }
                if (typeof value !== 'string') return _handleError('"value" must be a string.');
                this.store.setItem(key, value);
                return _key;
            };
            this.getItem = function (_key) {
                var key = this.getKey(_key);
                return this.store.getItem(key);
            };
            this.exists = function (_key) {
                var key = this.getKey(_key);
                return this.store.getItem(key) !== null;
            };
            this.removeItem = function (_key) {
                var key = this.getKey(_key);
                return this.store.removeItem(key);
            };
        };
        LocalStorage.getInstance = function () {
            if (!this._instance) {
                this._instance = new LocalStorage();
            }
            return this._instance;
        };

        /**
         * LocalStorage main interface constructor
         * @memberof TableExport.prototype
         * @constructor
         */
        var RowColMap = function () {
            this.rcMap = [];
            this.merges = [];

            this.isIgnore = function (ir, ic) {
                var _ignore = RowColMap.prototype.TYPE.IGNORE;
                return this.getRowColMapProp(ir, ic, _ignore);
            };
            this.isEmpty = function (ir, ic) {
                var _empty = RowColMap.prototype.TYPE.EMPTY;
                return this.getRowColMapProp(ir, ic, _empty);
            };
            this.isRowSpan = function (ir) {
                var _rowspan = RowColMap.prototype.TYPE.ROWSPAN;
                return this.getRowColMapProp(ir, undefined, _rowspan);
            };
            this.isColSpan = function (ir) {
                var _colspan = RowColMap.prototype.TYPE.COLSPAN;
                return this.getRowColMapProp(ir, undefined, _colspan);
            };
            this.isSpan = function (ir) {
                return this.isRowSpan(ir) || this.isColSpan(ir);
            };
            this.isMerge = function (ir) {
                return this.merges.length > 0;
            };
            this.addMerge = function (ir, mergeObj) {
                var _merge = RowColMap.prototype.TYPE.MERGE;
                this.merges.push(mergeObj);
                this.setRowColMapProp(ir, undefined, _merge, this.merges);
            };
            this.getRowColMapProp = function (ir, ic, key) {
                if (this.rcMap[ir]) {
                    if (typeof key === 'undefined') {
                        return this.rcMap[ir][ic];
                    } else if (typeof ic === 'undefined') {
                        return this.rcMap[ir][key];
                    } else if (this.rcMap[ir][ic]) {
                        return this.rcMap[ir][ic][key];
                    }
                }
                return undefined;
            };
            this.setRowColMapProp = function (ir, ic, key, value) {
                this.rcMap[ir] = this.rcMap[ir] || [];
                if (typeof key === 'undefined') {
                    return this.rcMap[ir][ic] = value;
                } else if (typeof ic === 'undefined') {
                    return this.rcMap[ir][key] = value;
                } else {
                    this.rcMap[ir][ic] = this.rcMap[ir][ic] || [];
                    return this.rcMap[ir][ic][key] = value;
                }
            };
            this.generateTotal = function (ir, ic) {
                var _total = 0;

                if (this.isRowSpan(ir) && this.isColSpan(ir)) {
                    _total = this.getRowColMapProp(ir, ic) || 0;
                } else if (this.getRowColMapProp(ir, ic)) {
                    _total = this.getRowColMapProp(ir, ic);
                }
                return _total;
            };
            this.convertSpanToArray = function (ir, ic, key, _return, colDel) {
                if (this.rcMap[ir] && this.isSpan(ir)) {
                    var total = this.generateTotal(ir, ic);

                    if (_isEnhanced(key)) {
                        _return.m = this.getRowColMapProp(ir, undefined, RowColMap.prototype.TYPE.MERGE);
                        return new Array(total).concat(_return);
                    } else {
                        new Array(total).concat(_return).join(colDel);
                    }
                }
                return _return;
            };
            this.handleRowColMapProp = function (type, ir, ic, key, _return, colDel) {
                switch (type) {
                    case RowColMap.prototype.TYPE.IGNORE:
                        return;
                    case RowColMap.prototype.TYPE.EMPTY:
                        return ' ';
                    case RowColMap.prototype.TYPE.DEFAULT:
                    default:
                        return this.convertSpanToArray(ir, ic, key, _return, colDel);
                }
            };
        };
        RowColMap.prototype = {
            OFFSET: 1,
            TYPE: {
                IGNORE: 'ignore',
                EMPTY: 'empty',
                MERGE: 'merge',
                ROWSPAN: 'rowspan',
                COLSPAN: 'colspan',
                COLSPANTOTAL: 'colspantotal',
                DEFAULT: 'default'
            },
            build: function (context, settings) {
                var self = this;

                var OFFSET = self.OFFSET;
                var rowLength = self.rowLength = context.rows.length;
                // var colLength = self.colLength = Math.max.apply(null,
                //     _nodesArray(context.rows).map(function (val) {
                //         return val.querySelectorAll('th, td').length
                //     }));

                var handleIgnore = function (ir, ic) {
                    self.setRowColMapProp(ir, ic, self.TYPE.IGNORE, true);
                };
                var handleEmpty = function (ir, ic) {
                    self.setRowColMapProp(ir, ic, self.TYPE.EMPTY, true);
                };
                var handleRowSpan = function (val, ir, ic) {
                    var rowSpan = +val.getAttribute('rowspan');
                    var hasColSpan = val.hasAttribute('colspan');
                    var handledByColSpan, countColSpan, totalColSpan;

                    for (var _row = 0; _row < rowSpan; _row++) {
                        if (_row + ir >= rowLength) {
                            return;
                        }
                        hasColSpan && (handledByColSpan = handleColSpan(val, _row + ir, ic, _row > 0, rowSpan));
                        if (_row >= 1) {
                            if (!handledByColSpan) {
                                totalColSpan = self.getRowColMapProp(ir, undefined, self.TYPE.COLSPANTOTAL) || 0;
                                countColSpan = self.getRowColMapProp(ir, undefined, self.TYPE.COLSPAN) || 0;
                                self.setRowColMapProp(_row + ir, ic + totalColSpan - countColSpan, undefined, 1);
                                // handleMerge(ir, icStart, irEnd, icEnd);
                            }
                        }
                    }
                };
                var handleColSpan = function (val, ir, ic, isRowSpan, rowSpan) {
                    var colSpan = +val.getAttribute('colspan');
                    var countColSpan = self.getRowColMapProp(ir, undefined, self.TYPE.COLSPAN) || 0;
                    var totalColSpan = self.getRowColMapProp(ir, undefined, self.TYPE.COLSPANTOTAL) || 0;

                    if (colSpan <= 1) {
                        return false;
                    }
                    self.setRowColMapProp(ir, undefined, self.TYPE.COLSPAN, countColSpan + 1);
                    self.setRowColMapProp(ir, undefined, self.TYPE.COLSPANTOTAL, totalColSpan + colSpan);

                    if (isRowSpan) {
                        self.setRowColMapProp(ir, ic - countColSpan, undefined, colSpan);
                        return true;
                    } else {
                        var irEnd = ir + (rowSpan || 1) - OFFSET;
                        var icStart = ic + totalColSpan - countColSpan;
                        var icEnd = ic + totalColSpan - countColSpan + (colSpan - OFFSET);
                        self.setRowColMapProp(ir, ic + OFFSET, undefined, colSpan - OFFSET);
                        handleMerge(ir, icStart, irEnd, icEnd);
                    }
                };

                var handleMerge = function (irs, ics, ire, ice) {
                    var merge = {
                        s: {r: irs, c: ics},
                        e: {r: ire, c: ice}
                    };
                    console.debug("merges:", merge.s, merge.e);
                    return self.addMerge(irs, merge);
                };

                _nodesArray(context.rows).map(function (val, ir) {
                    if (!!~settings.ignoreRows.indexOf(ir - context.thAdj) || _matches(val, settings.ignoreCSS)) {
                        handleIgnore(ir);
                    }
                    if (_matches(val, settings.emptyCSS)) {
                        handleEmpty(ir);
                    }
                    var cols = val.querySelectorAll('th, td');
                    return _nodesArray(cols).map(function (val, ic) {
                        if (!!~settings.ignoreCols.indexOf(ic) || _matches(val, settings.ignoreCSS)) {
                            handleIgnore(ir, ic);
                        }
                        if (_matches(val, settings.emptyCSS)) {
                            handleEmpty(ir, ic);
                        }
                        if (val.hasAttribute('rowspan')) {
                            handleRowSpan(val, ir, ic);
                        } else if (val.hasAttribute('colspan')) {
                            handleColSpan(val, ir, ic);
                        }
                    });
                });

                return self;
            }
        };

        var _isMobile = (function isMobile(ua) {
            return /(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i.test(ua)
                || /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(ua.substring(0, 4))
        })(navigator.userAgent || navigator.vendor || window.opera);

        var _isEnhanced = (function () {
            return function (key) {
                return (
                    XLSX
                    && !_isMobile
                    && !(TableExport.prototype.formatConfig[key].enforceStrictRFC4180)
                );
            }
        })();

        var _FORMAT = (function () {
            return TableExport.prototype.CONSTANTS.FORMAT;
        })();

        var _FORMAT_LIST = (function () {
            return Object.keys(_FORMAT).map(function (key) {
                return _FORMAT[key];
            });
        })();

        var _TYPE = (function () {
            return TableExport.prototype.CONSTANTS.TYPE;
        })();

        Array.prototype.processRows = function (key, rowDel) {
            if (_isEnhanced(key)) {
                return this.map(_toArray).filter(_defined);
            } else {
                return this.filter(_defined).join(rowDel);
            }
        };
        Array.prototype.processCols = function (key, colDel) {
            if (_isEnhanced(key)) {
                return this.filter(_defined);
            } else {
                return this.filter(_defined).join(colDel);
            }
        };

        var _uuid = (function () {
            var uuid = 0;

            return function (el) {
                if (!el.id) {
                    el.id = TableExport.prototype.defaultNamespace + (++uuid);
                }
                return el.id;
            };
        })();

        var _hashCode = (function () {
            var hash = 0, i, char;

            return function (hashKey) {
                var type = hashKey.type;
                hashKey = JSON.stringify(hashKey);
                if (hashKey.length === 0) return hash;
                for (i = 0; i < hashKey.length; i++) {
                    char = hashKey.charCodeAt(i);
                    hash = ((hash << 5) - hash) + char;
                    hash |= 0;
                }
                return hash.toString(16).substring(1) + '-' + type;
            }
        })();

        var _on = (function () {
            var prevFn = null;

            return function (el, event, fn, context) { // , args
                var curFn = fn.bind(context);
                // var curFn = fn.bind.apply(fn, [context].concat(args)); // OR [].slice.call(arguments[4]))
                for (var i = 0; i < el.length; ++i) {
                    prevFn && el[i].removeEventListener(event, prevFn, false);
                    el[i].addEventListener(event, curFn, false);
                }
                prevFn = curFn;
            }
        })();

        function _extend() {
            var args = arguments;
            for (var i = 1; i < args.length; i++)
                for (var key in args[i])
                    if (args[i].hasOwnProperty(key))
                        args[0][key] = args[i][key];
            return args[0];
        }

        function _nodesArray(els) {
            return (typeof els.length === 'undefined') ? [].concat(els) : [].slice.call(els);
        }

        function _hasClass(el, cls) {
            return el.classList ? el.classList.contains(cls) : new RegExp('(^| )' + cls + '( |$)', 'gi').test(el.cls);
        }

        function _matches(el, selectors) {
            return selectors.filter(function (selector) {
                    return [].indexOf.call(document.querySelectorAll(selector), el) !== -1;
                }).length > 0;
        }

        function _numeric(val) {
            return !isNaN(val);
        }

        function _defined(val) {
            return typeof val !== 'undefined';
        }

        function _toArray(val) {
            return val instanceof Array ? [].concat.apply([], val) : val;
        }

        function _isValidFormat(key) {
            return ~_FORMAT_LIST.indexOf(key);
        }

        function _hasDependencies(key) {
            var hasDependencies;

            switch (key) {
                case _FORMAT.TXT:
                case _FORMAT.CSV:
                case _FORMAT.XLS:
                    hasDependencies = true;
                    break;
                default:
                    hasDependencies = _isEnhanced(key);
            }
            return hasDependencies
        }

        function _handleError(msg) {
            console.error(msg);
            return new Error(msg)
        }

        function _getBootstrapSettings(bootstrap, bootstrapConfig, defaultButton) {
            var config = {};
            if (bootstrap) {
                config.bootstrapClass = bootstrapConfig[0] + ' ';
                config.bootstrapTheme = bootstrapConfig[1] + ' ';
                config.bootstrapSpacing = bootstrapConfig[2] + ' ';
            } else {
                config.bootstrapClass = defaultButton + ' ';
                config.bootstrapTheme = '';
                config.bootstrapSpacing = '';
            }
            return config;
        }

        if ($) {
            /**
             * jQuery TableExport wrapper
             * @param options {Object} TableExport configuration options
             * @returns {TableExport} TableExport instance
             */
            $.fn.tableExport = function (options) {
                return new TableExport(this, options);
            };

            // alias the TableExport prototype
            for (var prop in TableExport.prototype) {
                $.fn.tableExport[prop] = TableExport.prototype[prop];
            }
        }

        TableExport.TableExport = TableExport;

        return TableExport;
    }
));
