import { assign, each, filter, forEach, get, includes, isString, last, map, toString, isFinite } from 'lodash';

import { InterpolateFunction } from '@grafana/data';
import { isVersionGtOrEq } from 'app/core/utils/version';

export type ParamDef = {
  name: string;
  type: string;
  options?: Array<string | number>;
  multiple?: boolean;
  optional?: boolean;
  version?: string;
};

export interface FuncDef {
  name: string;
  params: ParamDef[];
  defaultParams: Array<string | number>;
  category?: string;
  shortName?: string;
  fake?: boolean;
  version?: string;
  description?: string;
  /**
   * True if the function was not found on the list of available function descriptions.
   */
  unknown?: boolean;
}

export type FuncDefs = {
  [functionName in string]: FuncDef;
};

const index: FuncDefs = {};

function addFuncDef(funcDef: Partial<FuncDef> & { name: string; category: string }) {
  funcDef.params = funcDef.params || [];
  funcDef.defaultParams = funcDef.defaultParams || [];

  index[funcDef.name] = funcDef as FuncDef;
  if (funcDef.shortName) {
    index[funcDef.shortName] = funcDef as FuncDef;
  }
}

const optionalSeriesRefArgs = [{ name: 'other', type: 'value_or_series', optional: true, multiple: true }];

addFuncDef({
  name: 'scaleToSeconds',
  category: 'Transform',
  params: [{ name: 'seconds', type: 'int' }],
  defaultParams: [1],
});

addFuncDef({
  name: 'perSecond',
  category: 'Transform',
  params: [{ name: 'max value', type: 'int', optional: true }],
  defaultParams: [],
});

addFuncDef({
  name: 'holtWintersForecast',
  category: 'Calculate',
});

addFuncDef({
  name: 'holtWintersConfidenceBands',
  category: 'Calculate',
  params: [{ name: 'delta', type: 'int' }],
  defaultParams: [3],
});

addFuncDef({
  name: 'holtWintersAberration',
  category: 'Calculate',
  params: [{ name: 'delta', type: 'int' }],
  defaultParams: [3],
});

addFuncDef({
  name: 'nPercentile',
  category: 'Calculate',
  params: [{ name: 'Nth percentile', type: 'int' }],
  defaultParams: [95],
});

addFuncDef({
  name: 'diffSeries',
  params: optionalSeriesRefArgs,
  defaultParams: ['#A'],
  category: 'Combine',
});

addFuncDef({
  name: 'stddevSeries',
  params: optionalSeriesRefArgs,
  defaultParams: [''],
  category: 'Combine',
});

addFuncDef({
  name: 'divideSeries',
  params: optionalSeriesRefArgs,
  defaultParams: ['#A'],
  category: 'Combine',
});

addFuncDef({
  name: 'multiplySeries',
  params: optionalSeriesRefArgs,
  defaultParams: ['#A'],
  category: 'Combine',
});

addFuncDef({
  name: 'asPercent',
  params: optionalSeriesRefArgs,
  defaultParams: ['#A'],
  category: 'Combine',
});

addFuncDef({
  name: 'group',
  params: optionalSeriesRefArgs,
  defaultParams: ['#A', '#B'],
  category: 'Combine',
});

addFuncDef({
  name: 'sumSeries',
  shortName: 'sum',
  category: 'Combine',
  params: optionalSeriesRefArgs,
  defaultParams: [''],
});

addFuncDef({
  name: 'averageSeries',
  shortName: 'avg',
  category: 'Combine',
  params: optionalSeriesRefArgs,
  defaultParams: [''],
});

addFuncDef({
  name: 'rangeOfSeries',
  category: 'Combine',
});

addFuncDef({
  name: 'percentileOfSeries',
  category: 'Combine',
  params: [
    { name: 'n', type: 'int' },
    { name: 'interpolate', type: 'boolean', options: ['true', 'false'] },
  ],
  defaultParams: [95, 'false'],
});

addFuncDef({
  name: 'sumSeriesWithWildcards',
  category: 'Combine',
  params: [{ name: 'node', type: 'int', multiple: true }],
  defaultParams: [3],
});

addFuncDef({
  name: 'maxSeries',
  shortName: 'max',
  category: 'Combine',
});

addFuncDef({
  name: 'minSeries',
  shortName: 'min',
  category: 'Combine',
});

addFuncDef({
  name: 'averageSeriesWithWildcards',
  category: 'Combine',
  params: [{ name: 'node', type: 'int', multiple: true }],
  defaultParams: [3],
});

addFuncDef({
  name: 'alias',
  category: 'Alias',
  params: [{ name: 'alias', type: 'string' }],
  defaultParams: ['alias'],
});

addFuncDef({
  name: 'aliasSub',
  category: 'Alias',
  params: [
    { name: 'search', type: 'string' },
    { name: 'replace', type: 'string' },
  ],
  defaultParams: ['', '\\1'],
});

addFuncDef({
  name: 'consolidateBy',
  category: 'Special',
  params: [
    {
      name: 'function',
      type: 'string',
      options: ['sum', 'average', 'min', 'max'],
    },
  ],
  defaultParams: ['max'],
});

addFuncDef({
  name: 'cumulative',
  category: 'Special',
  params: [],
  defaultParams: [],
});

addFuncDef({
  name: 'groupByNode',
  category: 'Combine',
  params: [
    {
      name: 'node',
      type: 'int',
      options: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 12],
    },
    {
      name: 'function',
      type: 'string',
      options: ['sum', 'avg', 'maxSeries'],
    },
  ],
  defaultParams: [3, 'sum'],
});

addFuncDef({
  name: 'aliasByNode',
  category: 'Alias',
  params: [
    {
      name: 'node',
      type: 'int',
      options: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 12],
      multiple: true,
    },
  ],
  defaultParams: [3],
});

addFuncDef({
  name: 'substr',
  category: 'Special',
  params: [
    {
      name: 'start',
      type: 'int',
      options: [-6, -5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 12],
    },
    {
      name: 'stop',
      type: 'int',
      options: [-6, -5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 12],
    },
  ],
  defaultParams: [0, 0],
});

addFuncDef({
  name: 'sortByName',
  category: 'Sorting',
  params: [
    {
      name: 'natural',
      type: 'boolean',
      options: ['true', 'false'],
      optional: true,
    },
  ],
  defaultParams: ['false'],
});

addFuncDef({
  name: 'sortByMaxima',
  category: 'Sorting',
});

addFuncDef({
  name: 'sortByMinima',
  category: 'Sorting',
});

addFuncDef({
  name: 'sortByTotal',
  category: 'Sorting',
});

addFuncDef({
  name: 'aliasByMetric',
  category: 'Alias',
});

addFuncDef({
  name: 'randomWalk',
  fake: true,
  category: 'Special',
  params: [{ name: 'name', type: 'string' }],
  defaultParams: ['randomWalk'],
});

addFuncDef({
  name: 'countSeries',
  category: 'Combine',
});

addFuncDef({
  name: 'constantLine',
  category: 'Special',
  params: [{ name: 'value', type: 'int' }],
  defaultParams: [10],
});

addFuncDef({
  name: 'cactiStyle',
  category: 'Special',
});

addFuncDef({
  name: 'keepLastValue',
  category: 'Transform',
  params: [{ name: 'n', type: 'int' }],
  defaultParams: [100],
});

addFuncDef({
  name: 'changed',
  category: 'Special',
  params: [],
  defaultParams: [],
});

addFuncDef({
  name: 'scale',
  category: 'Transform',
  params: [{ name: 'factor', type: 'int' }],
  defaultParams: [1],
});

addFuncDef({
  name: 'offset',
  category: 'Transform',
  params: [{ name: 'amount', type: 'int' }],
  defaultParams: [10],
});

addFuncDef({
  name: 'transformNull',
  category: 'Transform',
  params: [{ name: 'amount', type: 'int' }],
  defaultParams: [0],
});

addFuncDef({
  name: 'integral',
  category: 'Transform',
});

addFuncDef({
  name: 'derivative',
  category: 'Transform',
});

addFuncDef({
  name: 'nonNegativeDerivative',
  category: 'Transform',
  params: [{ name: 'max value or 0', type: 'int', optional: true }],
  defaultParams: [''],
});

addFuncDef({
  name: 'timeShift',
  category: 'Transform',
  params: [
    {
      name: 'amount',
      type: 'select',
      options: ['1h', '6h', '12h', '1d', '2d', '7d', '14d', '30d'],
    },
  ],
  defaultParams: ['1d'],
});

addFuncDef({
  name: 'timeStack',
  category: 'Transform',
  params: [
    {
      name: 'timeShiftUnit',
      type: 'select',
      options: ['1h', '6h', '12h', '1d', '2d', '7d', '14d', '30d'],
    },
    { name: 'timeShiftStart', type: 'int' },
    { name: 'timeShiftEnd', type: 'int' },
  ],
  defaultParams: ['1d', 0, 7],
});

addFuncDef({
  name: 'summarize',
  category: 'Transform',
  params: [
    { name: 'interval', type: 'string' },
    {
      name: 'func',
      type: 'select',
      options: ['sum', 'avg', 'min', 'max', 'last'],
    },
    {
      name: 'alignToFrom',
      type: 'boolean',
      optional: true,
      options: ['false', 'true'],
    },
  ],
  defaultParams: ['1h', 'sum', 'false'],
});

addFuncDef({
  name: 'smartSummarize',
  category: 'Transform',
  params: [
    { name: 'interval', type: 'string' },
    {
      name: 'func',
      type: 'select',
      options: ['sum', 'avg', 'min', 'max', 'last'],
    },
  ],
  defaultParams: ['1h', 'sum'],
});

addFuncDef({
  name: 'absolute',
  category: 'Transform',
});

addFuncDef({
  name: 'hitcount',
  category: 'Transform',
  params: [{ name: 'interval', type: 'string' }],
  defaultParams: ['10s'],
});

addFuncDef({
  name: 'log',
  category: 'Transform',
  params: [{ name: 'base', type: 'int' }],
  defaultParams: ['10'],
});

addFuncDef({
  name: 'averageAbove',
  category: 'Filter Series',
  params: [{ name: 'n', type: 'int' }],
  defaultParams: [25],
});

addFuncDef({
  name: 'averageBelow',
  category: 'Filter Series',
  params: [{ name: 'n', type: 'int' }],
  defaultParams: [25],
});

addFuncDef({
  name: 'currentAbove',
  category: 'Filter Series',
  params: [{ name: 'n', type: 'int' }],
  defaultParams: [25],
});

addFuncDef({
  name: 'currentBelow',
  category: 'Filter Series',
  params: [{ name: 'n', type: 'int' }],
  defaultParams: [25],
});

addFuncDef({
  name: 'maximumAbove',
  category: 'Filter Series',
  params: [{ name: 'value', type: 'int' }],
  defaultParams: [0],
});

addFuncDef({
  name: 'maximumBelow',
  category: 'Filter Series',
  params: [{ name: 'value', type: 'int' }],
  defaultParams: [0],
});

addFuncDef({
  name: 'minimumAbove',
  category: 'Filter Series',
  params: [{ name: 'value', type: 'int' }],
  defaultParams: [0],
});

addFuncDef({
  name: 'minimumBelow',
  category: 'Filter Series',
  params: [{ name: 'value', type: 'int' }],
  defaultParams: [0],
});

addFuncDef({
  name: 'limit',
  category: 'Filter Series',
  params: [{ name: 'n', type: 'int' }],
  defaultParams: [5],
});

addFuncDef({
  name: 'mostDeviant',
  category: 'Filter Series',
  params: [{ name: 'n', type: 'int' }],
  defaultParams: [10],
});

addFuncDef({
  name: 'exclude',
  category: 'Filter Series',
  params: [{ name: 'exclude', type: 'string' }],
  defaultParams: ['exclude'],
});

addFuncDef({
  name: 'highestCurrent',
  category: 'Filter Series',
  params: [{ name: 'count', type: 'int' }],
  defaultParams: [5],
});

addFuncDef({
  name: 'highestMax',
  category: 'Filter Series',
  params: [{ name: 'count', type: 'int' }],
  defaultParams: [5],
});

addFuncDef({
  name: 'lowestCurrent',
  category: 'Filter Series',
  params: [{ name: 'count', type: 'int' }],
  defaultParams: [5],
});

addFuncDef({
  name: 'movingAverage',
  category: 'Calculate',
  params: [
    {
      name: 'windowSize',
      type: 'int_or_interval',
      options: ['5', '7', '10', '5min', '10min', '30min', '1hour'],
    },
  ],
  defaultParams: [10],
});

addFuncDef({
  name: 'movingMedian',
  category: 'Calculate',
  params: [
    {
      name: 'windowSize',
      type: 'int_or_interval',
      options: ['5', '7', '10', '5min', '10min', '30min', '1hour'],
    },
  ],
  defaultParams: ['5'],
});

addFuncDef({
  name: 'stdev',
  category: 'Calculate',
  params: [
    { name: 'n', type: 'int' },
    { name: 'tolerance', type: 'int' },
  ],
  defaultParams: [5, 0.1],
});

addFuncDef({
  name: 'highestAverage',
  category: 'Filter Series',
  params: [{ name: 'count', type: 'int' }],
  defaultParams: [5],
});

addFuncDef({
  name: 'lowestAverage',
  category: 'Filter Series',
  params: [{ name: 'count', type: 'int' }],
  defaultParams: [5],
});

addFuncDef({
  name: 'removeAbovePercentile',
  category: 'Filter Data',
  params: [{ name: 'n', type: 'int' }],
  defaultParams: [5],
});

addFuncDef({
  name: 'removeAboveValue',
  category: 'Filter Data',
  params: [{ name: 'n', type: 'int' }],
  defaultParams: [5],
});

addFuncDef({
  name: 'removeBelowPercentile',
  category: 'Filter Data',
  params: [{ name: 'n', type: 'int' }],
  defaultParams: [5],
});

addFuncDef({
  name: 'removeBelowValue',
  category: 'Filter Data',
  params: [{ name: 'n', type: 'int' }],
  defaultParams: [5],
});

addFuncDef({
  name: 'useSeriesAbove',
  category: 'Filter Series',
  params: [
    { name: 'value', type: 'int' },
    { name: 'search', type: 'string' },
    { name: 'replace', type: 'string' },
  ],
  defaultParams: [0, 'search', 'replace'],
});

////////////////////
// Graphite 1.0.x //
////////////////////

addFuncDef({
  name: 'aggregateLine',
  category: 'Calculate',
  params: [
    {
      name: 'func',
      type: 'select',
      options: ['sum', 'avg', 'min', 'max', 'last'],
    },
  ],
  defaultParams: ['avg'],
  version: '1.0',
});

addFuncDef({
  name: 'averageOutsidePercentile',
  category: 'Filter Series',
  params: [{ name: 'n', type: 'int' }],
  defaultParams: [95],
  version: '1.0',
});

addFuncDef({
  name: 'delay',
  category: 'Transform',
  params: [{ name: 'steps', type: 'int' }],
  defaultParams: [1],
  version: '1.0',
});

addFuncDef({
  name: 'exponentialMovingAverage',
  category: 'Calculate',
  params: [
    {
      name: 'windowSize',
      type: 'int_or_interval',
      options: ['5', '7', '10', '5min', '10min', '30min', '1hour'],
    },
  ],
  defaultParams: [10],
  version: '1.0',
});

addFuncDef({
  name: 'fallbackSeries',
  category: 'Special',
  params: [{ name: 'fallback', type: 'string' }],
  defaultParams: ['constantLine(0)'],
  version: '1.0',
});

addFuncDef({
  name: 'grep',
  category: 'Filter Series',
  params: [{ name: 'grep', type: 'string' }],
  defaultParams: ['grep'],
  version: '1.0',
});

addFuncDef({
  name: 'groupByNodes',
  category: 'Combine',
  params: [
    {
      name: 'function',
      type: 'string',
      options: ['sum', 'avg', 'maxSeries'],
    },
    {
      name: 'node',
      type: 'int',
      options: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 12],
      multiple: true,
    },
  ],
  defaultParams: ['sum', 3],
  version: '1.0',
});

addFuncDef({
  name: 'integralByInterval',
  category: 'Transform',
  params: [
    {
      name: 'intervalUnit',
      type: 'select',
      options: ['1h', '6h', '12h', '1d', '2d', '7d', '14d', '30d'],
    },
  ],
  defaultParams: ['1d'],
  version: '1.0',
});

addFuncDef({
  name: 'interpolate',
  category: 'Transform',
  params: [{ name: 'limit', type: 'int', optional: true }],
  defaultParams: [],
  version: '1.0',
});

addFuncDef({
  name: 'invert',
  category: 'Transform',
  version: '1.0',
});

addFuncDef({
  name: 'isNonNull',
  category: 'Combine',
  version: '1.0',
});

addFuncDef({
  name: 'linearRegression',
  category: 'Calculate',
  params: [
    {
      name: 'startSourceAt',
      type: 'select',
      options: ['-1h', '-6h', '-12h', '-1d', '-2d', '-7d', '-14d', '-30d'],
      optional: true,
    },
    {
      name: 'endSourceAt',
      type: 'select',
      options: ['-1h', '-6h', '-12h', '-1d', '-2d', '-7d', '-14d', '-30d'],
      optional: true,
    },
  ],
  defaultParams: [],
  version: '1.0',
});

addFuncDef({
  name: 'mapSeries',
  shortName: 'map',
  params: [{ name: 'node', type: 'int' }],
  defaultParams: [3],
  category: 'Combine',
  version: '1.0',
});

addFuncDef({
  name: 'movingMin',
  category: 'Calculate',
  params: [
    {
      name: 'windowSize',
      type: 'int_or_interval',
      options: ['5', '7', '10', '5min', '10min', '30min', '1hour'],
    },
  ],
  defaultParams: [10],
  version: '1.0',
});

addFuncDef({
  name: 'movingMax',
  category: 'Calculate',
  params: [
    {
      name: 'windowSize',
      type: 'int_or_interval',
      options: ['5', '7', '10', '5min', '10min', '30min', '1hour'],
    },
  ],
  defaultParams: [10],
  version: '1.0',
});

addFuncDef({
  name: 'movingSum',
  category: 'Calculate',
  params: [
    {
      name: 'windowSize',
      type: 'int_or_interval',
      options: ['5', '7', '10', '5min', '10min', '30min', '1hour'],
    },
  ],
  defaultParams: [10],
  version: '1.0',
});

addFuncDef({
  name: 'multiplySeriesWithWildcards',
  category: 'Combine',
  params: [
    {
      name: 'position',
      type: 'int',
      options: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 12],
      multiple: true,
    },
  ],
  defaultParams: [2],
  version: '1.0',
});

addFuncDef({
  name: 'offsetToZero',
  category: 'Transform',
  version: '1.0',
});

addFuncDef({
  name: 'pow',
  category: 'Transform',
  params: [{ name: 'factor', type: 'int' }],
  defaultParams: [10],
  version: '1.0',
});

addFuncDef({
  name: 'powSeries',
  category: 'Transform',
  params: optionalSeriesRefArgs,
  defaultParams: [''],
  version: '1.0',
});

addFuncDef({
  name: 'reduceSeries',
  shortName: 'reduce',
  params: [
    {
      name: 'function',
      type: 'string',
      options: ['asPercent', 'diffSeries', 'divideSeries'],
    },
    {
      name: 'reduceNode',
      type: 'int',
      options: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13],
    },
    { name: 'reduceMatchers', type: 'string', multiple: true },
  ],
  defaultParams: ['asPercent', 2, 'used_bytes'],
  category: 'Combine',
  version: '1.0',
});

addFuncDef({
  name: 'removeBetweenPercentile',
  category: 'Filter Series',
  params: [{ name: 'n', type: 'int' }],
  defaultParams: [95],
  version: '1.0',
});

addFuncDef({
  name: 'removeEmptySeries',
  category: 'Filter Series',
  version: '1.0',
});

addFuncDef({
  name: 'squareRoot',
  category: 'Transform',
  version: '1.0',
});

addFuncDef({
  name: 'timeSlice',
  category: 'Transform',
  params: [
    {
      name: 'startSliceAt',
      type: 'select',
      options: ['-1h', '-6h', '-12h', '-1d', '-2d', '-7d', '-14d', '-30d'],
    },
    {
      name: 'endSliceAt',
      type: 'select',
      options: ['-1h', '-6h', '-12h', '-1d', '-2d', '-7d', '-14d', '-30d'],
      optional: true,
    },
  ],
  defaultParams: ['-1h'],
  version: '1.0',
});

addFuncDef({
  name: 'weightedAverage',
  category: 'Combine',
  params: [
    { name: 'other', type: 'value_or_series', optional: true },
    {
      name: 'node',
      type: 'int',
      options: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 12],
    },
  ],
  defaultParams: ['#A', 4],
  version: '1.0',
});

addFuncDef({
  name: 'seriesByTag',
  category: 'Special',
  params: [{ name: 'tagExpression', type: 'string', multiple: true }],
  version: '1.1',
});

addFuncDef({
  name: 'groupByTags',
  category: 'Combine',
  params: [
    {
      name: 'function',
      type: 'string',
      options: ['sum', 'avg', 'maxSeries'],
    },
    { name: 'tag', type: 'string', multiple: true },
  ],
  defaultParams: ['sum', 'tag'],
  version: '1.1',
});

addFuncDef({
  name: 'aliasByTags',
  category: 'Alias',
  params: [{ name: 'tag', type: 'string', multiple: true }],
  defaultParams: ['tag'],
  version: '1.1',
});

function isVersionRelatedFunction(obj: { version?: string }, graphiteVersion: string) {
  return !obj.version || isVersionGtOrEq(graphiteVersion, obj.version);
}

export class FuncInstance {
  def: FuncDef;
  params: Array<string | number>;
  text = '';
  /**
   * True if this function was just added and not edited yet. It's used to focus on first
   * function param to edit it straight away after adding a function.
   */
  declare added: boolean;
  /**
   * Hidden functions are not displayed in UI but available in text editor
   * This is used for seriesByTagUsed function which when used switches
   * the editor to tag-only mode. Defined tags are provided to seriesByTagUsed
   * as parameters.
   */
  hidden?: boolean;

  constructor(funcDef: FuncDef, options?: { withDefaultParams: boolean }) {
    this.def = funcDef;
    this.params = [];

    if (options && options.withDefaultParams && funcDef.defaultParams) {
      this.params = funcDef.defaultParams.slice(0);
    }

    this.updateText();
  }

  render(metricExp: string, replaceVariables: InterpolateFunction): string {
    const str = this.def.name + '(';

    const parameters = map(this.params, (value, index) => {
      let paramType;

      if (index < this.def.params.length) {
        paramType = this.def.params[index].type;
      } else if (get(last(this.def.params), 'multiple')) {
        paramType = get(last(this.def.params), 'type');
      }

      // param types that should never be quoted
      const neverQuotedParams = ['value_or_series', 'boolean', 'int', 'float', 'node', 'int_or_infinity'];

      // functions that should not have param types quoted
      // https://github.com/grafana/grafana/issues/54924
      const neverQuotedFunctions = ['asPercent'];
      // params or functions that should never be quoted
      if (includes(neverQuotedParams, paramType) || includes(neverQuotedFunctions, this.def.name)) {
        return value;
      }

      const valueInterpolated = isString(value) ? replaceVariables(value) : value;

      // param types that might be quoted
      // To quote variables correctly we need to interpolate it to check if it contains a numeric or string value
      if (includes(['int_or_interval', 'node_or_tag'], paramType) && isFinite(+valueInterpolated)) {
        return toString(value);
      }

      return "'" + value + "'";
    });

    // don't send any blank parameters to graphite
    while (parameters[parameters.length - 1] === '') {
      parameters.pop();
    }

    if (metricExp) {
      parameters.unshift(metricExp);
    }

    return str + parameters.join(', ') + ')';
  }

  _hasMultipleParamsInString(strValue: string, index: number) {
    if (strValue.indexOf(',') === -1) {
      return false;
    }

    if (this.def.params[index + 1] && this.def.params[index + 1].optional) {
      return true;
    }

    if (index + 1 >= this.def.params.length && get(last(this.def.params), 'multiple')) {
      return true;
    }

    return false;
  }

  updateParam(strValue: string, index: number) {
    // handle optional parameters
    // if string contains ',' and next param is optional, split and update both
    if (this._hasMultipleParamsInString(strValue, index)) {
      each(strValue.split(','), (partVal, idx) => {
        this.updateParam(partVal.trim(), index + idx);
      });
      return;
    }

    if (strValue === '' && (index >= this.def.params.length || this.def.params[index].optional)) {
      this.params.splice(index, 1);
    } else {
      this.params[index] = strValue;
    }

    this.updateText();
  }

  updateText() {
    if (this.params.length === 0) {
      this.text = this.def.name + '()';
      return;
    }

    let text = this.def.name + '(';
    text += this.params.join(', ');
    text += ')';
    this.text = text;
  }
}

function createFuncInstance(
  funcDef: FuncDef | string,
  options?: { withDefaultParams: boolean },
  idx?: FuncDefs | null
): FuncInstance {
  if (isString(funcDef)) {
    funcDef = getFuncDef(funcDef, idx);
  }
  return new FuncInstance(funcDef, options);
}

function getFuncDef(name: string, idx?: FuncDefs | null): FuncDef {
  if (!(idx || index)[name]) {
    return { name: name, params: [{ name: '', type: '', multiple: true }], defaultParams: [''], unknown: true };
  }
  return (idx || index)[name];
}

function getFuncDefs(graphiteVersion: string, idx?: FuncDefs | null): FuncDefs {
  const funcs: FuncDefs = {};
  forEach(idx || index, (funcDef: FuncDef) => {
    if (isVersionRelatedFunction(funcDef, graphiteVersion)) {
      funcs[funcDef.name] = assign({}, funcDef, {
        params: filter(funcDef.params, (param) => {
          return isVersionRelatedFunction(param, graphiteVersion);
        }),
      });
    }
  });
  return funcs;
}

// parse response from graphite /functions endpoint into internal format
function parseFuncDefs(rawDefs: any): FuncDefs {
  const funcDefs: FuncDefs = {};

  forEach(rawDefs || {}, (funcDef, funcName) => {
    // skip graphite graph functions
    if (funcDef.group === 'Graph') {
      return;
    }

    let description = funcDef.description;
    if (description) {
      // tidy up some pydoc syntax that rst2html can't handle
      description = description
        .replace(/:py:func:`(.+)( <[^>]*>)?`/g, '``$1``')
        .replace(/.. seealso:: /g, 'See also: ')
        .replace(/.. code-block *:: *none/g, '.. code-block::');
    }

    const func: FuncDef = {
      name: funcDef.name,
      description,
      category: funcDef.group,
      params: [],
      defaultParams: [],
      fake: false,
    };

    // get rid of the first "seriesList" param
    if (/^seriesLists?$/.test(get(funcDef, 'params[0].type', ''))) {
      // handle functions that accept multiple seriesLists
      // we leave the param in place but mark it optional, so users can add more series if they wish
      if (funcDef.params[0].multiple) {
        funcDef.params[0].required = false;
        // otherwise chop off the first param, it'll be handled separately
      } else {
        funcDef.params.shift();
      }
      // tag function as fake
    } else {
      func.fake = true;
    }

    forEach(funcDef.params, (rawParam) => {
      const param: ParamDef = {
        name: rawParam.name,
        type: 'string',
        optional: !rawParam.required,
        multiple: !!rawParam.multiple,
        options: undefined,
      };

      if (rawParam.default !== undefined) {
        if (rawParam.default === Infinity) {
          func.defaultParams.push('inf');
        } else {
          func.defaultParams.push(toString(rawParam.default));
        }
      } else if (rawParam.suggestions) {
        func.defaultParams.push(toString(rawParam.suggestions[0]));
      } else {
        func.defaultParams.push('');
      }

      if (rawParam.type === 'boolean') {
        param.type = 'boolean';
        param.options = ['true', 'false'];
      } else if (rawParam.type === 'integer') {
        param.type = 'int';
      } else if (rawParam.type === 'float') {
        param.type = 'float';
      } else if (rawParam.type === 'node') {
        param.type = 'node';
        param.options = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'];
      } else if (rawParam.type === 'nodeOrTag') {
        param.type = 'node_or_tag';
        param.options = ['name', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'];
      } else if (rawParam.type === 'intOrInterval') {
        param.type = 'int_or_interval';
      } else if (rawParam.type === 'seriesList') {
        param.type = 'value_or_series';
      } else if (rawParam.type === 'intOrInf') {
        param.type = 'int_or_infinity';
      }

      if (rawParam.options) {
        param.options = map(rawParam.options, toString);
      } else if (rawParam.suggestions) {
        param.options = map(rawParam.suggestions, toString);
      }

      func.params.push(param);
    });

    funcDefs[funcName] = func;
  });

  return funcDefs;
}

export default {
  createFuncInstance: createFuncInstance,
  getFuncDef: getFuncDef,
  getFuncDefs: getFuncDefs,
  parseFuncDefs: parseFuncDefs,
};
