/**
 * Registry for row actions
 *
 * Plugins can call DataTable_RowActions_Registry.register() from their JS
 * files in order to add new actions to arbitrary data tables. The register()
 * method takes an object containing:
 * - name: string identifying the action. must be short, no spaces.
 * - dataTableIcon: path to the icon for the action
 * - createInstance: a factory method to create an instance of the appropriate
 *                   subclass of DataTable_RowAction
 * - isAvailable: a method to determine whether the action is available in a
 *                given row of a data table
 */
var DataTable_RowActions_Registry = {

    registry: [],

    register: function (action) {
        var createInstance = action.createInstance;
        action.createInstance = function (dataTable, param) {
            var instance = createInstance(dataTable, param);
            instance.actionName = action.name;
            return instance;
        };

        this.registry.push(action);
    },

    getAvailableActionsForReport: function (dataTableParams, tr) {
        if (dataTableParams.disable_row_actions == '1') {
            return [];
        }

        var available = [];
        for (var i = 0; i < this.registry.length; i++) {
            if (this.registry[i].isAvailableOnReport(dataTableParams, tr)) {
                available.push(this.registry[i]);
            }
        }
        available.sort(function (a, b) {
            return b.order - a.order;
        });
        return available;
    },

    getActionByName: function (name) {
        for (var i = 0; i < this.registry.length; i++) {
            if (this.registry[i].name == name) {
                return this.registry[i];
            }
        }
        return false;
    }

};

// Register Row Evolution (also servers as example)
DataTable_RowActions_Registry.register({

    name: 'RowEvolution',

    dataTableIcon: 'icon-evolution',

    order: 50,

    dataTableIconTooltip: [
        _pk_translate('General_RowEvolutionRowActionTooltipTitle'),
        _pk_translate('General_RowEvolutionRowActionTooltip')
    ],

    createInstance: function (dataTable, param) {
        if (dataTable !== null && typeof dataTable.rowEvolutionActionInstance != 'undefined') {
            return dataTable.rowEvolutionActionInstance;
        }

        if (dataTable === null && param) {
            // when row evolution is triggered from the url (not a click on the data table)
            // we look for the data table instance in the dom
            // This actually doesn't work very good, as opening a row evolution using url params
            // directly also triggers loading the report datatable, which might not yet be finished at
            // this state, so the datatable might not yet be available
            // When migrating/refactoring this it might be good to use promises in some way, so it would
            // be possible to actually trigger the row evolution popover once the origin report was loaded.
            var report = param.split(':')[0];
            var div = $(require('piwik/UI').DataTable.getDataTableByReport(report));
            if (div.length && div.data('uiControlObject')) {
                dataTable = div.data('uiControlObject');
                if (typeof dataTable.rowEvolutionActionInstance != 'undefined') {
                    return dataTable.rowEvolutionActionInstance;
                }
            }
        }

        var instance = new DataTable_RowActions_RowEvolution(dataTable);
        if (dataTable !== null) {
            dataTable.rowEvolutionActionInstance = instance;
        }
        return instance;
    },

    isAvailableOnReport: function (dataTableParams) {
        return (
            typeof dataTableParams.disable_row_evolution == 'undefined'
            || dataTableParams.disable_row_evolution == "0"
        );
    },

    isAvailableOnRow: function (dataTableParams, tr) {
        return !tr.hasClass('totalsRow');
    }

});

/**
 * DataTable Row Actions
 *
 * The lifecycle of an action is as follows:
 * - for each data table, a new instance of the action is created using the factory
 * - when the table is loaded, initTr is called for each tr
 * - when the action icon is clicked, trigger is called
 * - the label is put together and performAction is called
 * - performAction must call openPopover on the base class
 * - openPopover calls back doOpenPopover after doing general stuff
 *
 * The two template methods are performAction and doOpenPopover
 */

//
// BASE CLASS
//

function DataTable_RowAction(dataTable) {
    this.dataTable = dataTable;

    // has to be overridden in subclasses
    this.trEventName = 'piwikTriggerRowAction';

    // set in registry
    this.actionName = 'RowAction';
}

/** Initialize a row when the table is loaded */
DataTable_RowAction.prototype.initTr = function (tr) {
    var self = this;

    // For subtables, we need to make sure that the actions are always triggered on the
    // action instance connected to the root table. Otherwise sharing data (e.g. for
    // for multi-row evolution) wouldn't be possible. Also, sub-tables might have different
    // API actions. For the label filter to work, we need to use the parent action.
    // We use jQuery events to let subtables access their parents.
    tr.unbind(self.trEventName).bind(self.trEventName, function (e, params) {
        self.trigger($(this), params.originalEvent, params.label, params.originalRow);
    });
};

/**
 * This method is called from the click event and the tr event (see this.trEventName).
 * It derives the label and calls performAction.
 */
DataTable_RowAction.prototype.trigger = function (tr, e, subTableLabel, originalRow) {
    var label = this.getLabelFromTr(tr);

    // if we have received the event from the sub table, add the label
    if (subTableLabel) {
        var separator = ' > '; // LabelFilter::SEPARATOR_RECURSIVE_LABEL
        label += separator + subTableLabel;
    }

    // handle sub tables in nested reports: forward to parent
    var subtable = tr.closest('table');
    if (subtable.is('.subDataTable')) {
        subtable.closest('tr').prev().trigger(this.trEventName, {
            label: label,
            originalEvent: e,
            originalRow: tr
        });
        return;
    }

    // ascend in action reports
    var $dataTable = subtable.closest('div.dataTable');
    if ($dataTable.hasClass('dataTableActions')
      || $dataTable.data('table-type') === 'ActionsDataTable'
    ) {
        var allClasses = tr.attr('class');
        var matches = allClasses.match(/level[0-9]+/);
        var level = parseInt(matches[0].substring(5, matches[0].length), 10);
        if (level > 0) {
            // .prev(.levelX) does not work for some reason => do it "by hand"
            var findLevel = 'level' + (level - 1);
            var ptr = tr;
            while ((ptr = ptr.prev()).length) {
                if (!ptr.hasClass(findLevel) || ptr.hasClass('nodata')) {
                    continue;
                }
                ptr.trigger(this.trEventName, {
                    label: label,
                    originalEvent: e,
                    originalRow: tr
                });
                return;
            }
        }
    }

    this.performAction(label, tr, e, originalRow);
};

/** Get the label string from a tr dom element */
DataTable_RowAction.prototype.getLabelFromTr = function (tr) {
    if (tr.data('label')) {
        return tr.data('label');
    }

    var rowMetadata = this.getRowMetadata(tr);
    if (rowMetadata.combinedLabel) {
        return '@' + rowMetadata.combinedLabel;
    }

    var label = tr.find('span.label');

    // handle truncation
    var value = label.data('originalText');

    if (!value) {
        value = label.text();
    }
    value = value.trim();
    value = encodeURIComponent(value);

    // if tr is a terminal node, we use the @ operator to distinguish it from branch nodes w/ the same name
    if (!tr.hasClass('subDataTable')) {
        value = '@' + value;
    }

    return value;
};

/** Get row metadata object */
DataTable_RowAction.prototype.getRowMetadata = function (tr) {
    return tr.data('row-metadata') || {};
};

/**
 * Base method for opening popovers.
 * This method will remember the parameter in the url and call doOpenPopover().
 */
DataTable_RowAction.prototype.openPopover = function (parameter) {
    broadcast.propagateNewPopoverParameter('RowAction', this.actionName + ':' + parameter);
};

broadcast.addPopoverHandler('RowAction', function (param) {
    var paramParts = param.split(':');
    var rowActionName = paramParts[0];
    paramParts.shift();
    param = paramParts.join(':');

    var rowAction = DataTable_RowActions_Registry.getActionByName(rowActionName);
    if (rowAction) {
        rowAction.createInstance(null, param).doOpenPopover(param);
    }
});

/** To be overridden */
DataTable_RowAction.prototype.performAction = function (label, tr, e) {
};
DataTable_RowAction.prototype.doOpenPopover = function (parameter) {
};

//
// ROW EVOLUTION
//

function DataTable_RowActions_RowEvolution(dataTable) {
    this.dataTable = dataTable;
    this.trEventName = 'piwikTriggerRowEvolution';

    /** The rows to be compared in multi row evolution */
    this.multiEvolutionRows = [];
    this.multiEvolutionRowsPretty = [];
    this.multiEvolutionRowsSeries = [];
    this.popoverRequestParams = null;
    this._popoverRequest = null;
    this._themeModeChangeListener = null;
    this._popoverRequestSequence = 0;
}

/** Static helper method to launch row evolution from anywhere */
DataTable_RowActions_RowEvolution.launch = function (apiMethod, label) {
    var param = 'RowEvolution:' + apiMethod + ':0:' + label;
    broadcast.propagateNewPopoverParameter('RowAction', param);
};

DataTable_RowActions_RowEvolution.prototype = new DataTable_RowAction;

DataTable_RowActions_RowEvolution.prototype.performAction = function (label, tr, e, originalRow) {
    if (e.shiftKey) {
        // only mark for multi row evolution if shift key is pressed
        this.addMultiEvolutionRow(label, $(originalRow || tr).data('comparison-series'), originalRow || tr);
        return;
    }

    this.addMultiEvolutionRow(label, $(originalRow || tr).data('comparison-series'), originalRow || tr);

    // check whether we have rows marked for multi row evolution
    var extraParams = $.extend({}, $(originalRow || tr).data('param-override'));
    if (typeof extraParams !== 'object') {
        extraParams = {};
    }

    if (this.multiEvolutionRows.length > 1) {
        extraParams.action = 'getMultiRowEvolutionPopover';
        label = this.multiEvolutionRows.join(',');

        labelPretty = this.multiEvolutionRowsPretty.join(',');
        if (label != labelPretty) {
            extraParams.labelPretty = labelPretty;
        }

        if (this.multiEvolutionRowsSeries.length > 1) { // when comparison is active
            var MatomoUrl = window.CoreHome.MatomoUrl;
            extraParams.compareDates = MatomoUrl.parsed.value.compareDates;
            extraParams.comparePeriods = MatomoUrl.parsed.value.comparePeriods;
            extraParams.compareSegments = MatomoUrl.parsed.value.compareSegments;
            extraParams.labelSeries = this.multiEvolutionRowsSeries.join(',');

            // remove override period/date/segment since we are sending compare params so we can have the whole set of comparison
            // serieses for LabelFilter
            delete extraParams.period;
            delete extraParams.date;
            delete extraParams.segment;
        }
    } else {
      var labelPretty = this.getPrettyLabel(originalRow || tr);
      if (labelPretty && labelPretty != label) {
        extraParams['labelPretty'] = labelPretty;
      }
    }

    $.each(this.dataTable.param, function (index, value) {
        // we automatically add fields like idDimension, idGoal etc.
        if (DataTable_RowActions_RowEvolution.isAllowedIdExtraParam(index, value)) {
            extraParams[index] = value;
        }
    });

    if (this.dataTable && this.dataTable.jsViewDataTable === 'tableGoals') {
        // When there is a idGoal parameter available, the user is currently viewing a Goal or Ecommerce page
        // In this case we want to show the specific goal metrics in the row evolution
        if (extraParams['idGoal']) {
            extraParams['showGoalMetricsForGoal'] = extraParams['idGoal'];
            delete(extraParams['idGoal']);
        }
        // If no idGoal is available it is a random report switched to goal visualization
        // we then ensure the row evolution will show the goal overview metrics
        else  {
            extraParams['showGoalMetricsForGoal'] = -1;
        }
    }

    // check if abandonedCarts is in the dataTable params and if so, propagate to row evolution request
    if (this.dataTable.param.abandonedCarts !== undefined) {
        extraParams['abandonedCarts'] = this.dataTable.param.abandonedCarts;
    }

    if (this.dataTable.param.secondaryDimension !== undefined) {
        extraParams['secondaryDimension'] = this.dataTable.param.secondaryDimension;
    }

    if (this.dataTable.param.flat !== undefined) {
        var unflattenActionLabel = function(label) {
            // To "unflatten" a label we need to convert labels from e.g.
            // * @%2Fblog%2Fauthor%2Fjulien%2F
            // * @%2Fcontact
            // * @%2Fanalytics%2Fcultizer
            // to e.g.
            // * blog > author > julien > @%2Findex
            // * @%2Fcontact
            // * analytics > @%2Fcultizer
            return label.split(',').map(function(item) {
                var startsWithAt  = item.startsWith('@');
                if (startsWithAt ) {
                    item = item.slice(1);
                }

                item = decodeURIComponent(item);

                if (item === '/') {
                    return (startsWithAt  ? '@' : '') + encodeURIComponent('/index');
                }

                var isIndex = false;

                if (item.endsWith('/')) {
                    item = item.slice(0, -1);
                    isIndex = true;
                }
                if (item.startsWith('/')) {
                    item = item.slice(1);
                }

                var parts = item.split('/').map(encodeURIComponent);

                if (isIndex) {
                    parts.push(encodeURIComponent('/index'));
                } else {
                    parts[parts.length - 1] = '/' + parts[parts.length - 1];
                }

                if (startsWithAt ) {
                    parts[parts.length - 1] = '@' + parts[parts.length - 1];
                }

                return parts.join(' > ');
            }).join(',');
        };

        if (
          this.dataTable.param.module === 'Actions' && this.dataTable.param.action === 'getPageUrls'
          && this.dataTable.param.flat && label.indexOf(' > ') === -1
        ) {
            // Requesting a row evolution for a flattened page url report can easily reach memory limits
            // This happens due to the fact, that requesting a report flattened, will currently process
            // the data for ALL subtables, for all periods shown in the row evolution.
            // We actually would only need to fetch the data for the requested labels.
            // Till this was refactored in the backend, this hack will convert the flattened request
            // into a request that would come from a subtable. This is handled differently by the backend
            // and will only process the requested labels in the backend.
            label = unflattenActionLabel(label);
            extraParams['flat'] = 0;
        } else {
            extraParams['flat'] = this.dataTable.param.flat;
        }
    }

    var apiMethod = this.dataTable.param.module + '.' + this.dataTable.param.action;
    this.openPopover(apiMethod, extraParams, label);
};

DataTable_RowActions_RowEvolution.prototype.getPrettyLabel = function getPrettyLabel(tr) {
  if (!this.dataTable.props.row_identifier || this.dataTable.props.row_identifier === 'label') {
    return null; // only necessary if a custom row identifier is provided for the report
  }

  var prettyLabel = [];

  var row = $(tr);
  while (row.length) {
    var label = row.data('label-pretty') || this.getLabelFromTr(row);
    prettyLabel.unshift(label);

    var subtable = row.closest('table');
    if (subtable.is('.subDataTable')) {
      row = subtable.closest('tr').prev();
    } else {
      break;
    }
  }

  return prettyLabel.join(' > ');
};

DataTable_RowActions_RowEvolution.prototype.addMultiEvolutionRow = function (label, seriesIndex, tr) {
    if (typeof seriesIndex !== 'undefined') {
        var self = this;

        var found = false;
        this.multiEvolutionRows.forEach(function (rowLabel, index) {
            var rowSeriesIndex = self.multiEvolutionRowsSeries[index];
            if (label === rowLabel && seriesIndex === rowSeriesIndex) {
                found = true;
                return false;
            }
        });

        if (!found) {
            this.multiEvolutionRows.push(label);
            this.multiEvolutionRowsPretty.push(this.getPrettyLabel(tr));
            this.multiEvolutionRowsSeries.push(seriesIndex);
        }
    } else if ($.inArray(label, this.multiEvolutionRows) === -1) {
        this.multiEvolutionRows.push(label);
        this.multiEvolutionRowsPretty.push(this.getPrettyLabel(tr))

        this.multiEvolutionRowsSeries = []; // for safety, make sure state is consistent
    }
};

DataTable_RowActions_RowEvolution.prototype.openPopover = function (apiMethod, extraParams, label) {
    var urlParam = apiMethod + ':' + encodeURIComponent(JSON.stringify(extraParams)) + ':' + label;
    DataTable_RowAction.prototype.openPopover.apply(this, [urlParam]);
};

// Allowlist of `extraParams` keys that may flow from the popover URL hash into
// the Row Evolution XHR.
DataTable_RowActions_RowEvolution.allowedExtraParamKeys = [
    'column',
    'action',
    'labelPretty',
    'labelSeries',
    'showGoalMetricsForGoal',
    'abandonedCarts',
    'secondaryDimension',
    'flat',
    'compareDates',
    'comparePeriods',
    'compareSegments',
    'segment',
    'period',
    'date'
];

// Dynamic `id*` keys (idGoal, idDimension, idSubtable, ...). Used both when
// building the row-evolution URL in `performAction` and when parsing it back
// in `doOpenPopover`, so both sides accept the same set of params.
DataTable_RowActions_RowEvolution.isAllowedIdExtraParam = function (key, value) {
    return key !== 'idSite'
        && key.indexOf('id') === 0
        && ($.isNumeric(value) || (typeof value === 'string' && value.indexOf('ecommerce') === 0));
};

// Filter a parsed extraParams object against the allowlist. Returns a new
// object containing only keys that are either in `allowedExtraParamKeys` or
// accepted by `isAllowedIdExtraParam`. The `action` key is additionally pinned
// to the multi-row marker value.
DataTable_RowActions_RowEvolution.filterAllowedExtraParams = function (parsed) {
    var result = {};
    var allowed = DataTable_RowActions_RowEvolution.allowedExtraParamKeys;

    for (var key in parsed) {
        if (!Object.prototype.hasOwnProperty.call(parsed, key)) {
            continue;
        }

        var value = parsed[key];

        // `action` is allowed only as the multi-row marker. Layer 2 in
        // showRowEvolution unconditionally pins requestParams.action, but
        // the value-check keeps the in-memory extraParams honest.
        if (key === 'action' && value !== 'getMultiRowEvolutionPopover') {
            continue;
        }

        if (allowed.indexOf(key) !== -1
            || DataTable_RowActions_RowEvolution.isAllowedIdExtraParam(key, value)) {
            result[key] = value;
        }
    }

    return result;
};

DataTable_RowActions_RowEvolution.prototype.doOpenPopover = function (urlParam) {
    var urlParamParts = urlParam.split(':');
    var apiMethod = urlParamParts.shift();
    var extraParamsString = urlParamParts.shift();
    var label = urlParamParts.join(':');

    var extraParams = {};
    try {
        var parsed = JSON.parse(decodeURIComponent(extraParamsString));
        if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
            throw new Error('extraParams must be a JSON object');
        }
        extraParams = DataTable_RowActions_RowEvolution.filterAllowedExtraParams(parsed);
    } catch (e) {
        // Legacy short-form: bare "0"/"1"/<column-name> instead of a JSON envelope.
        if (extraParamsString == '1') {
            extraParams.action = 'getMultiRowEvolutionPopover';
        } else if (extraParamsString != '0') {
            extraParams.action = 'getMultiRowEvolutionPopover';
            extraParams.column = String(extraParamsString);
        }
    }

    this.showRowEvolution(apiMethod, label, extraParams);
};

DataTable_RowActions_RowEvolution.prototype.fetchRowEvolution = function (requestParams, callback) {
    var ajaxRequest = new ajaxHelper();
    ajaxRequest.addParams(requestParams, 'get');
    ajaxRequest.withTokenInUrl();
    ajaxRequest.setCallback(callback);
    ajaxRequest.setFormat('html');
    ajaxRequest.send();
    return ajaxRequest;
};

DataTable_RowActions_RowEvolution.prototype.abortPopoverRequest = function () {
    if (this._popoverRequest) {
        this._popoverRequest.abort();
        this._popoverRequest = null;
    }
};

DataTable_RowActions_RowEvolution.prototype.loadLatestPopover = function (requestParams, callback) {
    var self = this;
    var requestSequence;

    this.abortPopoverRequest();
    requestSequence = ++this._popoverRequestSequence;
    this._popoverRequest = this.fetchRowEvolution(requestParams, function (html) {
        self._popoverRequest = null;

        if (!Piwik_Popover.isOpen() || requestSequence !== self._popoverRequestSequence) {
            return;
        }

        callback(html);
    });
};

/** Open the row evolution popover */
DataTable_RowActions_RowEvolution.prototype.showRowEvolution = function (apiMethod, label, extraParams) {

    var self = this;

    // open the popover
    var box = Piwik_Popover.showLoading('Row Evolution');
    box.addClass('rowEvolutionPopover');

    // prepare loading the popover contents
    var requestParams = {
        apiMethod: apiMethod,
        label: label,
        disableLink: 1
    };

    var callback = function (html) {
        Piwik_Popover.setContent(html);

        // use the popover title returned from the server
        var title = box.find('div.popover-title');
        if (title.length) {
            Piwik_Popover.setTitle(title.html());
            title.remove();
        }

        Piwik_Popover.onClose(function () {
            if (!Piwik_Popover.isOpen()) {
                // reset rows marked for multi row evolution on close
                self.multiEvolutionRows = [];
                self.multiEvolutionRowsPretty = [];
                self.multiEvolutionRowsSeries = [];
                self.popoverRequestParams = null;
                self._popoverRequestSequence++;
                self.abortPopoverRequest();

                if (self._themeModeChangeListener) {
                    window.removeEventListener('themeModeChange', self._themeModeChangeListener);
                    self._themeModeChangeListener = null;
                }
            }
        });

        if (self.dataTable !== null) {
            // remember label for multi row evolution
            box.find('.rowevolution-startmulti').click(function () {
                Piwik_Popover.onClose(false); // unbind listener that resets multiEvolutionRows
                broadcast.propagateNewPopoverParameter(false);
                return false;
            });
        } else {
            // when the popover is launched by copy&pasting a url, we don't have the data table.
            // in this case, we can't remember the row marked for multi row evolution so
            // we disable the picker.
            box.find('.compare-container, .rowevolution-startmulti').remove();
        }

        // switch metric in multi row evolution
        box.find('select.multirowevoltion-metric').change(function () {
            var metric = $(this).val();
            Piwik_Popover.onClose(false); // unbind listener that resets multiEvolutionRows
            extraParams.column = metric;
            self.openPopover(apiMethod, extraParams, label);
            return true;
        });
    };

    requestParams.colors = JSON.stringify(piwik.getSparklineColors());

    var idDimension;

    if (broadcast.getValueFromUrl('module') === 'Widgetize') {
        idDimension = broadcast.getValueFromUrl('subcategory');
    } else {
        idDimension = broadcast.getValueFromHash('subcategory');
    }

    if (idDimension && ('' + idDimension).indexOf('customdimension') === 0) {
        idDimension = ('' + idDimension).replace('customdimension', '');
        idDimension = parseInt(idDimension, 10);
        if (idDimension > 0) {
            requestParams.idDimension = idDimension;
        }
    }

    var wantMultiRowEvolution = extraParams && extraParams.action === 'getMultiRowEvolutionPopover';

    $.extend(requestParams, extraParams);

    // Pin routing AFTER the merge so it cannot be overridden by any path.
    requestParams.module = 'CoreHome';
    requestParams.action = wantMultiRowEvolution ? 'getMultiRowEvolutionPopover' : 'getRowEvolutionPopover';

    this.popoverRequestParams = $.extend(true, {}, requestParams);

    if (this._themeModeChangeListener) {
        window.removeEventListener('themeModeChange', this._themeModeChangeListener);
    }

    this._themeModeChangeListener = function () {
        if (!self.popoverRequestParams) {
            return;
        }

        self.popoverRequestParams.colors = JSON.stringify(piwik.getSparklineColors());
        self.loadLatestPopover(self.popoverRequestParams, callback);
    };

    window.addEventListener('themeModeChange', this._themeModeChangeListener);

    this.loadLatestPopover(requestParams, callback);
};
