# modules/cell.js

    /*global jsPDF */
    /**
     * @license
     * ====================================================================
     * Copyright (c) 2013 Youssef Beddad, youssef.beddad@gmail.com
     *               2013 Eduardo Menezes de Morais, eduardo.morais@usp.br
     *               2013 Lee Driscoll, https://github.com/lsdriscoll
     *               2014 Juan Pablo Gaviria, https://github.com/juanpgaviria
     *               2014 James Hall, james@parall.ax
     *               2014 Diego Casorran, https://github.com/diegocr
     *
     * Permission is hereby granted, free of charge, to any person obtaining
     * a copy of this software and associated documentation files (the
     * "Software"), to deal in the Software without restriction, including
     * without limitation the rights to use, copy, modify, merge, publish,
     * distribute, sublicense, and/or sell copies of the Software, and to
     * permit persons to whom the Software is furnished to do so, subject to
     * the following conditions:
     *
     * The above copyright notice and this permission notice shall be
     * included in all copies or substantial portions of the Software.
     *
     * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
     * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
     * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
     * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
     * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
     * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
     * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
     * ====================================================================
     */

    /**
    * @name cell
    * @module
    */
    (function (jsPDFAPI) {
        'use strict';

        var NO_MARGINS = { left: 0, top: 0, bottom: 0, right: 0 };

        var px2pt = 0.264583 * 72 / 25.4;
        var printingHeaderRow = false;

        var _initialize = function () {
            if (typeof this.internal.__cell__ === "undefined") {
                this.internal.__cell__ = {};
                this.internal.__cell__.padding = 3;
                this.internal.__cell__.headerFunction = undefined;
                this.internal.__cell__.margins = Object.assign({}, NO_MARGINS);
                this.internal.__cell__.margins.width = this.getPageWidth();
                _reset.call(this);
            }
        };

        var _reset = function () {
            this.internal.__cell__.lastCell = new Cell();
            this.internal.__cell__.pages = 1;
        };

        var Cell = function () {
            var _x = arguments[0];
            Object.defineProperty(this, 'x', {
                enumerable: true,
                get: function () {
                    return _x;
                },
                set: function (value) {
                    _x = value;
                }
            });
            var _y = arguments[1];
            Object.defineProperty(this, 'y', {
                enumerable: true,
                get: function () {
                    return _y;
                },
                set: function (value) {
                    _y = value;
                }
            });
            var _width = arguments[2];
            Object.defineProperty(this, 'width', {
                enumerable: true,
                get: function () {
                    return _width;
                },
                set: function (value) {
                    _width = value;
                }
            });
            var _height = arguments[3];
            Object.defineProperty(this, 'height', {
                enumerable: true,
                get: function () {
                    return _height;
                },
                set: function (value) {
                    _height = value;
                }
            });
            var _text = arguments[4];
            Object.defineProperty(this, 'text', {
                enumerable: true,
                get: function () {
                    return _text;
                },
                set: function (value) {
                    _text = value;
                }
            });
            var _lineNumber = arguments[5];
            Object.defineProperty(this, 'lineNumber', {
                enumerable: true,
                get: function () {
                    return _lineNumber;
                },
                set: function (value) {
                    _lineNumber = value;
                }
            });
            var _align = arguments[6];
            Object.defineProperty(this, 'align', {
                enumerable: true,
                get: function () {
                    return _align;
                },
                set: function (value) {
                    _align = value;
                }
            });

            return this;
        };

        Cell.prototype.clone = function () {
            return new Cell(this.x, this.y, this.width, this.height, this.text, this.lineNumber, this.align);
        };

        Cell.prototype.toArray = function () {
            return [this.x, this.y, this.width, this.height, this.text, this.lineNumber, this.align];
        };

        /**
        * @name setHeaderFunction
        * @function
        * @param {function} func
        */
        jsPDFAPI.setHeaderFunction = function (func) {
            _initialize.call(this);
            this.internal.__cell__.headerFunction = (typeof func === 'function') ? func : undefined;
            return this;
        };

        /**
        * @name getTextDimensions
        * @function
        * @param {string} txt
        * @returns {Object} dimensions
        */
        jsPDFAPI.getTextDimensions = function (text, options) {
            _initialize.call(this);
            options = options || {};
            var fontSize = options.fontSize || this.getFontSize();
            var font = options.font || this.getFont();
            var scaleFactor = options.scaleFactor || this.internal.scaleFactor;
            var width = 0;
            var amountOfLines = 0;
            var height = 0;
            var tempWidth = 0;

            if (!Array.isArray(text) && typeof text !== 'string') {
                throw new Error('getTextDimensions expects text-parameter to be of type String or an Array of Strings.');
            }

            text = Array.isArray(text) ? text : [text];
            for (var i = 0; i < text.length; i++) {
                tempWidth = this.getStringUnitWidth(text[i], { font: font }) * fontSize;
                if (width < tempWidth) {
                    width = tempWidth;
                }
                if (width !== 0) {
                    amountOfLines = text.length;
                }
            }

            width = width / scaleFactor;
            height = Math.max((amountOfLines * fontSize * this.getLineHeightFactor() - (fontSize * (this.getLineHeightFactor() - 1))) / scaleFactor, 0);
            return { w: width, h: height };
        };

        /**
        * @name cellAddPage
        * @function
        */
        jsPDFAPI.cellAddPage = function () {
            _initialize.call(this);

            this.addPage();

            var margins = this.internal.__cell__.margins || NO_MARGINS;
            this.internal.__cell__.lastCell = new Cell(margins.left, margins.top, undefined, undefined);
            this.internal.__cell__.pages += 1;

            return this;
        };

        /**
        * @name cellInitialize
        * @function
        * @deprecated
        */
        jsPDFAPI.cellInitialize = function () {
            _initialize.call(this);
            _reset.call(this);
        };

        /**
        * @name cell
        * @function
        * @param {number} x
        * @param {number} y
        * @param {number} width
        * @param {number} height
        * @param {string} text
        * @param {number} lineNumber lineNumber
        * @param {string} align
        * @return {jsPDF} jsPDF-instance
        */
        var cell = jsPDFAPI.cell = function () {

            var currentCell;

            if (arguments[0] instanceof Cell) {
                currentCell = arguments[0];
            } else {
                currentCell = new Cell(arguments[0], arguments[1], arguments[2], arguments[3], arguments[4], arguments[5]);
            }
            _initialize.call(this);
            var lastCell = this.internal.__cell__.lastCell;
            var padding = this.internal.__cell__.padding;
            var margins = this.internal.__cell__.margins || NO_MARGINS;
            var tableHeaderRow = this.internal.__cell__.tableHeaderRow;
            var printHeaders = this.internal.__cell__.printHeaders;
            // If this is not the first cell, we must change its position
            if (typeof lastCell.lineNumber !== 'undefined') {
                if (lastCell.lineNumber === currentCell.lineNumber) {
                    //Same line
                    currentCell.x = (lastCell.x || 0) + (lastCell.width || 0);
                    currentCell.y = lastCell.y || 0;
                } else {
                    //New line
                    if (lastCell.y + lastCell.height + currentCell.height + margins.bottom > this.getPageHeight()) {
                        this.cellAddPage();
                        currentCell.y = margins.top;
                        if (printHeaders && tableHeaderRow) {
                            this.printHeaderRow(currentCell.lineNumber, true);
                            currentCell.y += tableHeaderRow[0].height;
                        }
                    } else {
                        currentCell.y = (lastCell.y + lastCell.height) || currentCell.y;
                    }
                }
            }

            if (typeof currentCell.text[0] !== 'undefined') {
                this.rect(currentCell.x, currentCell.y, currentCell.width, currentCell.height, (printingHeaderRow === true) ? 'FD' : undefined);
                if (currentCell.align === 'right') {
                    this.text(currentCell.text, currentCell.x + currentCell.width - padding, currentCell.y + padding, { align: 'right', baseline: 'top' });
                } else if (currentCell.align === 'center'){
                    this.text(currentCell.text, currentCell.x + currentCell.width / 2, currentCell.y + padding, { align: 'center', baseline: 'top', maxWidth: (currentCell.width - padding - padding) });
                } else {
                    this.text(currentCell.text, currentCell.x + padding, currentCell.y + padding, { align: 'left', baseline: 'top', maxWidth: (currentCell.width - padding - padding) });
                }
            }
            this.internal.__cell__.lastCell = currentCell;
            return this;
        };

        /**
         * Create a table from a set of data.
         * @name table
         * @function
         * @param {Integer} [x] : left-position for top-left corner of table
         * @param {Integer} [y] top-position for top-left corner of table
         * @param {Object[]} [data] An array of objects containing key-value pairs corresponding to a row of data.
         * @param {String[]} [headers] Omit or null to auto-generate headers at a performance cost

         * @param {Object} [config.printHeaders] True to print column headers at the top of every page
         * @param {Object} [config.autoSize] True to dynamically set the column widths to match the widest cell value
         * @param {Object} [config.margins] margin values for left, top, bottom, and width
         * @param {Object} [config.fontSize] Integer fontSize to use (optional)
         * @param {Object} [config.padding] cell-padding in pt to use (optional)
         * @param {Object} [config.headerBackgroundColor] default is #c8c8c8 (optional)
         * @returns {jsPDF} jsPDF-instance
         */

        jsPDFAPI.table = function (x, y, data, headers, config) {
            _initialize.call(this);
            if (!data) {
                throw new Error('No data for PDF table.');
            }

            config = config || {};

            var headerNames = [],
                headerLabels = [],
                headerAligns = [],
                i,
                columnMatrix = {},
                columnWidths = {},
                column,
                columnMinWidths = [],
                j,
                tableHeaderConfigs = [],

                //set up defaults. If a value is provided in config, defaults will be overwritten:
                autoSize = config.autoSize || false,
                printHeaders = (config.printHeaders === false) ? false : true,
                fontSize = (config.css && typeof (config.css['font-size']) !== "undefined") ? config.css['font-size'] * 16 : config.fontSize || 12,
                margins = config.margins || Object.assign({ width: this.getPageWidth() }, NO_MARGINS),
                padding = typeof config.padding === 'number' ? config.padding : 3,
                headerBackgroundColor = config.headerBackgroundColor || '#c8c8c8';

            _reset.call(this);

            this.internal.__cell__.printHeaders = printHeaders;
            this.internal.__cell__.margins = margins;
            this.internal.__cell__.table_font_size = fontSize;
            this.internal.__cell__.padding = padding;
            this.internal.__cell__.headerBackgroundColor = headerBackgroundColor;
            this.setFontSize(fontSize);

            // Set header values
            if (headers === undefined || (headers === null)) {
                // No headers defined so we derive from data
                headerNames = Object.keys(data[0]);
                headerLabels = headerNames;
                headerAligns = headerNames.map(function() {return 'left'});
            } else if (Array.isArray(headers) && (typeof headers[0] === 'object')) {
                headerNames = headers.map(function (header) {return header.name; });
                headerLabels = headers.map(function (header) { return header.prompt || header.name || ''});
                headerAligns = headerNames.map(function(header) {return header.align || 'left'});
                // Split header configs into names and prompts
                for (i = 0; i < headers.length; i += 1) {
                    columnWidths[headers[i].name] = headers[i].width * px2pt;
                }
            } else if (Array.isArray(headers) && (typeof headers[0] === 'string')) {
                headerNames = headers;
                headerLabels = headerNames;
                headerAligns = headerNames.map(function() {return 'left'});
            }

            if (autoSize) {
                var headerName;
                for (i = 0; i < headerNames.length; i += 1) {
                    headerName = headerNames[i];

                    // Create a matrix of columns e.g., {column_title: [row1_Record, row2_Record]}

                    columnMatrix[headerName] = data.map(function (rec) { return rec[headerName]; });

                    // get header width
                    this.setFontStyle('bold');
                    columnMinWidths.push(this.getTextDimensions(headerLabels[i], { fontSize: this.internal.__cell__.table_font_size, scaleFactor: this.internal.scaleFactor }).w);
                    column = columnMatrix[headerName];

                    // get cell widths
                    this.setFontStyle('normal');
                    for (j = 0; j < column.length; j += 1) {
                        columnMinWidths.push(this.getTextDimensions(column[j], { fontSize: this.internal.__cell__.table_font_size, scaleFactor: this.internal.scaleFactor }).w);
                    }

                    // get final column width
                    columnWidths[headerName] = Math.max.apply(null, columnMinWidths) + padding + padding;

                    //have to reset
                    columnMinWidths = [];
                }
            }

            // -- Construct the table

            if (printHeaders) {
                var row = {};
                for (i = 0; i < headerNames.length; i += 1) {
                    row[headerNames[i]] = {};
                    row[headerNames[i]].text = headerLabels[i];
                    row[headerNames[i]].align = headerAligns[i];
                }

                var rowHeight = calculateLineHeight.call(this, row, columnWidths);

                // Construct the header row
                tableHeaderConfigs = headerNames.map(function (value) { return new Cell(x, y, columnWidths[value], rowHeight, row[value].text, undefined, row[value].align ); });

                // Store the table header config
                this.setTableHeaderRow(tableHeaderConfigs);

                // Print the header for the start of the table
                this.printHeaderRow(1, false);
            }

            // Construct the data rows

            var align = headers.reduce(function (pv, cv) { pv[cv.name] = cv.align; return pv }, {});
            for (i = 0; i < data.length; i += 1) {
                var lineHeight = calculateLineHeight.call(this, data[i], columnWidths);

                for (j = 0; j < headerNames.length; j += 1) {
                    cell.call(this, new Cell(x, y, columnWidths[headerNames[j]], lineHeight, data[i][headerNames[j]], i + 2, align[headerNames[j]]));
                }
            }
            this.internal.__cell__.table_x = x;
            this.internal.__cell__.table_y = y;
            return this;
        };

        /**
         * Calculate the height for containing the highest column
         *
         * @name calculateLineHeight
         * @function
         * @param {Object[]} model is the line of data we want to calculate the height of
         * @param {Integer[]} columnWidths is size of each column
         * @returns {number} lineHeight
    	 * @private
         */
        var calculateLineHeight = function calculateLineHeight(model, columnWidths) {
            var padding = this.internal.__cell__.padding;
            var fontSize = this.internal.__cell__.table_font_size;
            var scaleFactor = this.internal.scaleFactor;

            return Object.keys(model)
                .map(function (value) {return typeof value === 'object' ? value.text : value})
                .map(function (value) { return this.splitTextToSize(value, columnWidths[value] - padding - padding) }, this)
                .map(function (value) { return this.getLineHeightFactor() * value.length * fontSize / scaleFactor + padding + padding }, this)
                .reduce(function (pv, cv) { return Math.max(pv, cv) }, 0);
        };

        /**
        * Store the config for outputting a table header
        *
        * @name setTableHeaderRow
        * @function
        * @param {Object[]} config
        * An array of cell configs that would define a header row: Each config matches the config used by jsPDFAPI.cell
        * except the lineNumber parameter is excluded
        */
        jsPDFAPI.setTableHeaderRow = function (config) {
            _initialize.call(this);
            this.internal.__cell__.tableHeaderRow = config;
        };

        /**
         * Output the store header row
         *
         * @name printHeaderRow
         * @function
         * @param {number} lineNumber The line number to output the header at
         * @param {boolean} new_page
         */
        jsPDFAPI.printHeaderRow = function (lineNumber, new_page) {
            _initialize.call(this);
            if (!this.internal.__cell__.tableHeaderRow) {
                throw new Error('Property tableHeaderRow does not exist.');
            }

            var tableHeaderCell;

            printingHeaderRow = true;
            if (typeof this.internal.__cell__.headerFunction === 'function') {
                var position = this.internal.__cell__.headerFunction(this, this.internal.__cell__.pages);
                this.internal.__cell__.lastCell = new Cell(position[0], position[1], position[2], position[3], undefined, -1);
            }
            this.setFontStyle('bold');

            var tempHeaderConf = [];
            for (var i = 0; i < this.internal.__cell__.tableHeaderRow.length; i += 1) {
                tableHeaderCell = this.internal.__cell__.tableHeaderRow[i].clone();
                if (new_page) {
                    tableHeaderCell.y = this.internal.__cell__.margins.top || 0;
                    tempHeaderConf.push(tableHeaderCell);
                }
                tableHeaderCell.lineNumber = lineNumber;
                this.setFillColor(this.internal.__cell__.headerBackgroundColor);
                cell.call(this, tableHeaderCell);
            }
            if (tempHeaderConf.length > 0) {
                this.setTableHeaderRow(tempHeaderConf);
            }
            this.setFontStyle('normal');
            printingHeaderRow = false;
        };

    })(jsPDF.API);