//################################################################################################
/** @file CSV mixin for ish.js
 * @mixin ish.io.csv
 * @author Nick Campbell
 * @license MIT
 * @copyright 2014-2023, Nick Campbell
 */ //############################################################################################
/*global module, define */                                      //# Enable Node globals for JSHint
/*jshint maxcomplexity:9 */                                     //# Enable max complexity warnings for JSHint
(function () {
    'use strict'; //<MIXIN>

    function init(core) {
        //################################################################################################
        /** Collection of CSV-based functionality.
         * @namespace ish.io.csv
         * @ignore
         */ //############################################################################################
        core.oop.partial(core.io, {
            csv: { // core.extend(core.resolve(true, core.data, "csv"), {
                //#########
                /** Parses the passed value into a Javascript object representing the CSV data.
                 * @function ish.io.csv.parse
                 * @param {string} sCSV Value representing the CSV data to parse.
                 * @param {string} [sDelimiter=','] Value representing the CSV delimiter.
                 * @returns {object[]} Value representing the CSV data.
                 * @see {@link https://stackoverflow.com/a/14991797/235704|StackOverflow.com}
                 */ //#####
                parse: function (sCSV, sDelimiter) {
                    var row, col, c, cc, nc,
                        arr = [],
                        quote = false  // true means we're inside a quoted field
                    ;

                    sDelimiter = sDelimiter || ",";

                    // iterate over each character, keep track of current row and column (of the returned array)
                    for (row = col = c = 0; c < sCSV.length; c++) {
                        cc = sCSV[c];                          // current character
                        nc = sCSV[c+1];                        // next character
                        arr[row] = arr[row] || [];             // create a new row if necessary
                        arr[row][col] = arr[row][col] || '';   // create a new column (start with empty string) if necessary

                        // If the current character is a quotation mark, and we're inside a
                        // quoted field, and the next character is also a quotation mark,
                        // add a quotation mark to the current column and skip the next character
                        if (cc == '"' && quote && nc == '"') { arr[row][col] += cc; ++c; continue; }

                        // If it's just one quotation mark, begin/end quoted field
                        if (cc == '"') { quote = !quote; continue; }

                        // If it's a sDelimiter and we're not in a quoted field, move on to the next column
                        if (cc == sDelimiter && !quote) { ++col; continue; }

                        // If it's a newline (CRLF) and we're not in a quoted field, skip the next character
                        // and move on to the next row and move to column 0 of that new row
                        if (cc == '\r' && nc == '\n' && !quote) { ++row; col = 0; ++c; continue; }

                        // If it's a newline (LF or CR) and we're not in a quoted field,
                        // move on to the next row and move to column 0 of that new row
                        if (cc == '\n' && !quote) { ++row; col = 0; continue; }
                        if (cc == '\r' && !quote) { ++row; col = 0; continue; }

                        // modification to allow proper handling of line feeds (per user655063)
                        if (cc == '\r' && nc == '\n' && !quote) { ++row; col = 0; ++c; continue; } if (cc == '\n' && !quote) { ++row; col = 0; continue; }

                        // Otherwise, append the current character to the current column
                        arr[row][col] += cc;
                    }
                    //return arr;

                    var i, j,
                        a_oReturnVal = []
                    ;

                    //#
                    a_oReturnVal.asArr = arr;

                    //# Traverse the arr, starting at the first data row, .push'ing a new object into our a_oReturnVal as we go
                    for (i = 1; i < arr.length; i++) {
                       a_oReturnVal.push({});

                        //# Traverse the arr's column header row
                        for (j = 0; j < arr[0].length; j++) {
                            //# Set the current a_oReturnVal's row under the current header column's name (or "ColumnJ+1")
                            a_oReturnVal[i - 1][
                                arr[0][j] || "Column" + (j + 1)
                            ] = arr[i][j];
                        }
                    }
                    return a_oReturnVal;
                }, //# io.csv.parse

                //#########
                /** Converts the passed value to a CSV string.
                 * @function ish.io.csv.stringify
                 * @param {object[]} a_oData Value representing the data to serialize into a CSV string.
                 * @param {string|object} [vOptions] Value representing the CSV delimiter or the desired options:
                 *      @param {string} [vOptions.delimiter=','] Value representing the CSV delimiter.
                 *      @param {boolean} [vOptions.quotes=false] Value representing each serialized value is to be surrounded by double-quotes (e.g. <code>"</code>).
                 *      @param {string[]} [vOptions.keys=undefined] Value representing the keys to include within the serialized CSV string.
                 * @returns {string} Value representing the CSV data.
                 */ //#####
                stringify: function (a_oData, vOptions) {
                    var a_sKeys, oOptions, vCurrent, iKeysLength, i, j,
                        sReturnVal = ""
                    ;

                    //#
                    oOptions = core.extend(
                        {
                            //keys: undefined,
                            quotes: false,
                            delimiter: ","
                        }, (
                            core.type.str.is(vOptions, true) ?
                                { delimiter: vOptions } :
                                vOptions
                        )
                    );
                    a_sKeys = oOptions.keys || core.type.obj.ownKeys(core.resolve(a_oData, "0"));
                    core.type.arr.rm(a_sKeys, "$$hashKey"); //# TODO: AngularJS specific
                    oOptions.delimiter = core.type.str.mk(oOptions.delimiter, ",");
                    oOptions.quotes = core.type.bool.is(oOptions.quotes, true);

                    //#
                    if (core.type.arr.is(a_sKeys, true)) {
                        iKeysLength = a_sKeys.length;
                        sReturnVal = a_sKeys.join(oOptions.delimiter) + "\n";

                        //#
                        if (core.type.arr.is(a_oData, true)) {
                            for (i = 0; i < a_oData.length; i++) {
                                for (j = 0; j < iKeysLength; j++) {
                                    //# Pass in a_sKeys[j] in an array so .resolve doesn't parse it for .'s
                                    vCurrent = core.resolve(a_oData[i], [a_sKeys[j]]);

                                    //#
                                    if (vCurrent === undefined) {
                                        if (!core.type.obj.has(a_oData[i], a_sKeys[j], false)) {
                                            vCurrent = "";
                                        }
                                    }
                                    //#
                                    else if (core.type.obj.is(vCurrent) || core.type.arr.is(vCurrent)) {
                                        vCurrent = '"' + JSON.stringify(vCurrent).replace(/"/g, '""') + '"';
                                    }
                                    //#
                                    else {
                                        //#
                                        if (core.type.is.numeric(vCurrent)) {
                                            vCurrent = core.type.str.mk(vCurrent);
                                        }

                                        //#
                                        if (oOptions.quotes || vCurrent.indexOf(oOptions.delimiter) > -1 || vCurrent.indexOf('"') > -1 || vCurrent.indexOf('\n') > -1) {
                                            vCurrent = '"' + vCurrent.replace(/"/g, '""') + '"';
                                        }
                                    }

                                    sReturnVal += vCurrent + ((iKeysLength - 1) === j ? "\n" : oOptions.delimiter);
                                }
                            }
                        }
                    }

                    return sReturnVal;
                } //# io.csv.stringify
            }
        }); //# core.io.csv

        //# .fire the plugin's loaded event
        core.io.event.fire("ish.io.csv");

        //# Return core to allow for chaining
        return core;
    } //# init


    //# If we are running server-side
    //#     NOTE: Compliant with UMD, see: https://github.com/umdjs/umd/blob/master/templates/returnExports.js
    //#     NOTE: Does not work with strict CommonJS, but only CommonJS-like environments that support module.exports, like Node.
    if (typeof module === 'object' && module.exports) { //if (typeof module !== 'undefined' && this.module !== module && module.exports) {
        module.exports = init;
    }
    //# Else if we are running in an .amd environment, register as an anonymous module
    else if (typeof define === 'function' && define.amd) {
        define([], init);
    }
    //# Else we are running in the browser, so we need to setup the _document-based features
    else {
        return init(window.head.ish || document.querySelector("SCRIPT[ish]").ish);
    }

    //</MIXIN>
}());