import { c3_chart_internal_fn } from './core';
import { isValue, isUndefined, isDefined, notEmpty, isArray } from './util';

c3_chart_internal_fn.convertUrlToData = function (url, mimeType, headers, keys, done) {
    var $$ = this, type = mimeType ? mimeType : 'csv';
    var req = $$.d3.request(url);
    if (headers) {
        Object.keys(headers).forEach(function (header) {
            req.header(header, headers[header]);
        });
    }
    req.get(function (error, data) {
        var d;
        var dataResponse = data.response || data.responseText; // Fixes IE9 XHR issue; see #1345
        if (!data) {
            throw new Error(error.responseURL + ' ' + error.status + ' (' + error.statusText + ')');
        }
        if (type === 'json') {
            d = $$.convertJsonToData(JSON.parse(dataResponse), keys);
        } else if (type === 'tsv') {
            d = $$.convertTsvToData(dataResponse);
        } else {
            d = $$.convertCsvToData(dataResponse);
        }
        done.call($$, d);
    });
};
c3_chart_internal_fn.convertXsvToData = function (xsv, parser) {
    var [ keys, ...rows ] = parser.parseRows(xsv);
    if (rows.length === 0) {
        return { keys, rows: [ keys.reduce((row, key) => Object.assign(row, { [key]: null }), {}) ] };
    } else {
        // [].concat() is to convert result into a plain array otherwise
        // test is not happy because rows have properties.
        return { keys, rows: [].concat(parser.parse(xsv)) };
    }
};
c3_chart_internal_fn.convertCsvToData = function (csv) {
    return this.convertXsvToData(csv, { parse: this.d3.csvParse, parseRows: this.d3.csvParseRows });
};
c3_chart_internal_fn.convertTsvToData = function (tsv) {
    return this.convertXsvToData(tsv, { parse: this.d3.tsvParse, parseRows: this.d3.tsvParseRows });
};
c3_chart_internal_fn.convertJsonToData = function (json, keys) {
    var $$ = this,
        new_rows = [], targetKeys, data;
    if (keys) { // when keys specified, json would be an array that includes objects
        if (keys.x) {
            targetKeys = keys.value.concat(keys.x);
            $$.config.data_x = keys.x;
        } else {
            targetKeys = keys.value;
        }
        new_rows.push(targetKeys);
        json.forEach(function (o) {
            var new_row = [];
            targetKeys.forEach(function (key) {
                // convert undefined to null because undefined data will be removed in convertDataToTargets()
                var v = $$.findValueInJson(o, key);
                if (isUndefined(v)) {
                    v = null;
                }
                new_row.push(v);
            });
            new_rows.push(new_row);
        });
        data = $$.convertRowsToData(new_rows);
    } else {
        Object.keys(json).forEach(function (key) {
            new_rows.push([key].concat(json[key]));
        });
        data = $$.convertColumnsToData(new_rows);
    }
    return data;
};
c3_chart_internal_fn.findValueInJson = function (object, path) {
    path = path.replace(/\[(\w+)\]/g, '.$1'); // convert indexes to properties (replace [] with .)
    path = path.replace(/^\./, '');           // strip a leading dot
    var pathArray = path.split('.');
    for (var i = 0; i < pathArray.length; ++i) {
        var k = pathArray[i];
        if (k in object) {
            object = object[k];
        } else {
            return;
        }
    }
    return object;
};

/**
 * Converts the rows to normalized data.
 * @param {any[][]} rows The row data
 * @return {Object}
 */
c3_chart_internal_fn.convertRowsToData = (rows) => {
    const newRows = [];
    const keys = rows[0];

    for (let i = 1; i < rows.length; i++) {
        const newRow = {};
        for (let j = 0; j < rows[i].length; j++) {
            if (isUndefined(rows[i][j])) {
                throw new Error("Source data is missing a component at (" + i + "," + j + ")!");
            }
            newRow[keys[j]] = rows[i][j];
        }
        newRows.push(newRow);
    }
    return { keys, rows: newRows };
};

/**
 * Converts the columns to normalized data.
 * @param {any[][]} columns The column data
 * @return {Object}
 */
c3_chart_internal_fn.convertColumnsToData = (columns) => {
    const newRows = [];
    const keys = [];

    for (let i = 0; i < columns.length; i++) {
        const key = columns[i][0];
        for (let j = 1; j < columns[i].length; j++) {
            if (isUndefined(newRows[j - 1])) {
                newRows[j - 1] = {};
            }
            if (isUndefined(columns[i][j])) {
                throw new Error("Source data is missing a component at (" + i + "," + j + ")!");
            }
            newRows[j - 1][key] = columns[i][j];
        }
        keys.push(key);
    }

    return { keys, rows: newRows };
};

/**
 * Converts the data format into the target format.
 * @param {!Object} data
 * @param {!Array} data.keys Ordered list of target IDs.
 * @param {!Array} data.rows Rows of data to convert.
 * @param {boolean} appendXs True to append to $$.data.xs, False to replace.
 * @return {!Array}
 */
c3_chart_internal_fn.convertDataToTargets = function (data, appendXs) {
    var $$ = this, config = $$.config, targets, ids, xs, keys;

    // handles format where keys are not orderly provided
    if (isArray(data)) {
        keys = Object.keys(data[ 0 ]);
    } else {
        keys = data.keys;
        data = data.rows;
    }

    ids = keys.filter($$.isNotX, $$);
    xs = keys.filter($$.isX, $$);

    // save x for update data by load when custom x and c3.x API
    ids.forEach(function (id) {
        var xKey = $$.getXKey(id);

        if ($$.isCustomX() || $$.isTimeSeries()) {
            // if included in input data
            if (xs.indexOf(xKey) >= 0) {
                $$.data.xs[id] = (appendXs && $$.data.xs[id] ? $$.data.xs[id] : []).concat(
                    data.map(function (d) { return d[xKey]; })
                        .filter(isValue)
                        .map(function (rawX, i) { return $$.generateTargetX(rawX, id, i); })
                );
            }
            // if not included in input data, find from preloaded data of other id's x
            else if (config.data_x) {
                $$.data.xs[id] = $$.getOtherTargetXs();
            }
            // if not included in input data, find from preloaded data
            else if (notEmpty(config.data_xs)) {
                $$.data.xs[id] = $$.getXValuesOfXKey(xKey, $$.data.targets);
            }
            // MEMO: if no x included, use same x of current will be used
        } else {
            $$.data.xs[id] = data.map(function (d, i) { return i; });
        }
    });


    // check x is defined
    ids.forEach(function (id) {
        if (!$$.data.xs[id]) {
            throw new Error('x is not defined for id = "' + id + '".');
        }
    });

    // convert to target
    targets = ids.map(function (id, index) {
        var convertedId = config.data_idConverter(id);
        return {
            id: convertedId,
            id_org: id,
            values: data.map(function (d, i) {
                var xKey = $$.getXKey(id), rawX = d[xKey],
                    value = d[id] !== null && !isNaN(d[id]) ? +d[id] : null, x;
                // use x as categories if custom x and categorized
                if ($$.isCustomX() && $$.isCategorized() && !isUndefined(rawX)) {
                    if (index === 0 && i === 0) {
                        config.axis_x_categories = [];
                    }
                    x = config.axis_x_categories.indexOf(rawX);
                    if (x === -1) {
                        x = config.axis_x_categories.length;
                        config.axis_x_categories.push(rawX);
                    }
                } else {
                    x  = $$.generateTargetX(rawX, id, i);
                }
                // mark as x = undefined if value is undefined and filter to remove after mapped
                if (isUndefined(d[id]) || $$.data.xs[id].length <= i) {
                    x = undefined;
                }
                return {x: x, value: value, id: convertedId};
            }).filter(function (v) { return isDefined(v.x); })
        };
    });

    // finish targets
    targets.forEach(function (t) {
        var i;
        // sort values by its x
        if (config.data_xSort) {
            t.values = t.values.sort(function (v1, v2) {
                var x1 = v1.x || v1.x === 0 ? v1.x : Infinity,
                    x2 = v2.x || v2.x === 0 ? v2.x : Infinity;
                return x1 - x2;
            });
        }
        // indexing each value
        i = 0;
        t.values.forEach(function (v) {
            v.index = i++;
        });
        // this needs to be sorted because its index and value.index is identical
        $$.data.xs[t.id].sort(function (v1, v2) {
            return v1 - v2;
        });
    });

    // cache information about values
    $$.hasNegativeValue = $$.hasNegativeValueInTargets(targets);
    $$.hasPositiveValue = $$.hasPositiveValueInTargets(targets);

    // set target types
    if (config.data_type) {
        $$.setTargetType($$.mapToIds(targets).filter(function (id) { return ! (id in config.data_types); }), config.data_type);
    }

    // cache as original id keyed
    targets.forEach(function (d) {
        $$.addCache(d.id_org, d);
    });

    return targets;
};
