/**
 * This base class contains utility methods for dealing with formats such as CSV (Comma
 * Separated Values) as specified in <a href="http://tools.ietf.org/html/rfc4180">RFC 4180</a>.
 *
 * The base class implements the mechanics and is governed by these config options:
 *
 *  * `{@link #delimiter}`
 *  * `{@link #lineBreak}`
 *  * `{@link #quote}`
 *
 * These options affect the `{@link #method-encode}` and `{@link #method-decode}` methods.
 * When *decoding*, however, `{@link #lineBreak}` is ignored and instead each line can
 * be separated by any standard line terminator character or character sequence:
 *
 *  * ```\u000a```
 *  * ```\u000d```
 *  * ```\u000d\u000a```
 *
 * Strings which contain the {@link #delimiter} character are quoted using the
 * {@link #quote} character, and any internal {@link #quote} characters are doubled.
 *
 * *Important*
 * While the primary use case is to encode strings, other atomic data types can be encoded
 * as values within a line such as:
 *
 *  * Number
 *  * Boolean
 *  * Date (encoded as an <a href="http://www.iso.org/iso/home/standards/iso8601.htm">ISO 8601</a> date string.)
 *  * null (encoded as an empty string.)
 *  * undefined (encoded as an empty string.)
 *
 * Not that when *decoding*, all data is read as strings. This class does not convert
 * incoming data. To do that, use an {@link Ext.data.reader.Array ArrayReader}.
 *
 * See `{@link Ext.util.CSV}` and  `{@link Ext.util.TSV}` for pre-configured instances.
 *
 * @since 5.1.0
 */
Ext.define('Ext.util.DelimitedValue', {
    /**
     * @cfg {String} dateFormat
     * The {@link Ext.Date#format format} to use for dates
     */
    dateFormat: 'C',
 
    /**
     * @cfg {String} delimiter
     * The string used to separate the values in a row. Common values for this config
     * are comma (",") and tab ("\t"). See `{@link Ext.util.CSV}` and  `{@link Ext.util.TSV}`
     * for pre-configured instances of these formats.
     */
    delimiter: '\t',
 
    /**
     * @cfg {String} lineBreak
     * The string used by `{@link #encode}` to separate each row. The `{@link #decode}`
     * method accepts all forms of line break.
     */
    lineBreak: '\n',
 
    /**
     * @cfg {String} quote
     * The character to use as to quote values that contain the special `delimiter`
     * or `{@link #lineBreak}` characters.
     */
    quote: '"',
 
    lineBreakRe: /\r?\n/g,
    // match line break at end of input
    lastLineBreakRe: /(\r?\n|\r)$/,
 
    constructor: function (config) {
        if (config) {
            Ext.apply(this, config);
        }
        
        this.parseREs = {};
        this.quoteREs = {};
    },
 
    /**
     * Decodes a string of encoded values into an array of rows. Each row is an array of
     * strings.
     *
     * Note that this function does not convert the string values in each column into
     * other data types. To do that, use an {@link Ext.data.reader.Array ArrayReader}.
     *
     * For example:
     *
     *     Ext.util.CSV.decode('"foo ""bar"", bletch",Normal String,2010-01-01T21:45:32.004Z\u000a3.141592653589793,1,false');
     *
     * produces the following array of string arrays:
     *
     *     [
     *         ['foo "bar", bletch','Normal String', '2010-01-01T21:45:32.004Z'],
     *         ['3.141592653589793', '1', 'false']
     *     ]
     *
     * @param {String} input The string to parse.
     *
     * @param {String} [delimiter] The column delimiter to use if the default value
     * of {@link #cfg-delimiter delimiter} is not desired.
     *
     * @param {String} [quoteChar] The character used to quote fields. Defaults to
     * double quote ("). Pass `null` to suppress field quoting.
     *
     * @return {String[][]} An array of rows where each row is an array of Strings.
     */
    decode: function(input, delimiter, quoteChar) {
    /* eslint-enable max-len */
        if (!input) {
            return [];
        }
 
        // eslint-disable-next-line vars-on-top
        var me = this,
            // Check to see if the column delimiter is defined. If not,
            // then default to comma.
            row = [],
            result = [row],
            quote = quoteChar !== undefined ? quoteChar : me.quote,
            quoteREs = me.quoteREs,
            parseREs = me.parseREs,
            parseRE, dblQuoteRE, arrMatches, strMatchedDelimiter, strMatchedValue;
 
        delimiter = delimiter || me.delimiter;
        // Create a regular expression to parse the CSV values unless we already have
        // one for this delimiter/quoteChar.
        parseRE = parseREs[delimiter] || new RegExp(
            // Delimiters.
            '(\\' + delimiter + '|\\r?\\n|\\r|^)' +
 
                // (Optionally) Quoted fields.
                '(?:\\' + quote + '([^\\' + quote + ']*(?:\\' + quote + '\\' + quote +
                    '[^\\' + quote + ']*)*)\\' + quote + '|' +
 
                // Standard fields.
                '([^\\' + delimiter + '\\r\\n]*))',
            'gi');
 
        dblQuoteRE = quoteREs[quote] ||
            (quoteREs[quote] = new RegExp('\\' + quote + '\\' + quote, 'g'));
 
        // Keep looping over the regular expression matches
        // until we can no longer find a match.
        while ((arrMatches = parseRE.exec(input))) {
            strMatchedDelimiter = arrMatches[1];
 
            // Check to see if the given delimiter has a length
            // (is not the start of string) and if it matches
            // field delimiter. If id does not, then we know
            // that this delimiter is a row delimiter.
            if (strMatchedDelimiter.length && strMatchedDelimiter !== delimiter) {
                // Since we have reached a new row of data,
                // add an empty row to our data array.
                result.push(row = []);
            }
 
            // we need to account for the first value being empty
            if (!arrMatches.index && arrMatches[0].charAt(0) === delimiter) {
                row.push('');
            }
 
            // Now that we have our delimiter out of the way,
            // let's check to see which kind of value we
            // captured (quoted or unquoted).
            if (arrMatches[2]) {
                // We found a quoted value. When we capture
                // this value, unescape any double quotes.
                strMatchedValue = arrMatches[2].replace(dblQuoteRE, quote);
            }
            else {
                // We found a non-quoted value.
                strMatchedValue = arrMatches[3];
            }
 
            row.push(strMatchedValue);
        }
 
        return result;
    },
 
    /**
     * Converts a two-dimensional array into an encoded string.
     *
     * For example:
     *
     *     Ext.util.CSV.encode([
     *         ['foo "bar", bletch', 'Normal String', new Date()],
     *         [Math.PI, 1, false]
     *     ]);
     *
     * The above produces the following string:
     *
     *     '"foo ""bar"", bletch",Normal String,2010-01-01T21:45:32.004Z\u000a3.141592653589793,1,false'
     *
     * @param {Mixed[][]} input An array of row data arrays.
     *
     * @param {String} [delimiter] The column delimiter to use if the default value
     * of {@link #cfg-delimiter delimiter} is not desired.
     *
     * @param {String} [quoteChar] The character used to quote fields. Defaults to
     * double quote ("). Pass `null` to suppress field quoting.
     *
     * @return {String} A string in which data items are separated by {@link #delimiter}
     * characters, and rows are separated by {@link #lineBreak} characters.
     */
    encode: function (input, delimiter, quoteChar) {
        var me = this,
            delim = delimiter || me.delimiter,
            dateFormat = me.dateFormat,
            quote = quoteChar !== undefined ? quoteChar : me.quote,
            twoQuotes = quote + quote,
            rowIndex = input.length,
            lineBreakRe = me.lineBreakRe,
            result = [],
            outputRow = [],
            col, columnIndex, inputRow;
 
        while (rowIndex-- > 0) {
            inputRow = input[rowIndex];
            outputRow.length = columnIndex = inputRow.length;
 
            while (columnIndex-- > 0) {
                col = inputRow[columnIndex];
 
                if (col == null) { // == null || === undefined
                    col = '';
                }
                else if (typeof col === 'string') {
                    if (col && quote !== null) {
                        // If the value contains quotes, double them up, and wrap with quotes
                        if (col.indexOf(quote) > -1) {
                            col = quote + col.split(quote).join(twoQuotes) + quote;
                        }
                        else if (col.indexOf(delim) > -1 || lineBreakRe.test(col)) {
                            col = quote + col + quote;
                        }
                    }
                }
                else if (Ext.isDate(col)) {
                    col = Ext.Date.format(col, dateFormat);
                }
                //<debug>
                else if (col && (isNaN(col) || Ext.isArray(col))) {
                    Ext.raise('Cannot serialize ' + Ext.typeOf(col) + ' into CSV');
                }
                //</debug>
 
                outputRow[columnIndex] = col;
            }
 
            result[rowIndex] = outputRow.join(delim);
        }
 
        return result.join(me.lineBreak);
    }
});