import { clone, map } from 'lodash';

import { functionRenderer, QueryPart, QueryPartDef, suffixRenderer } from 'app/features/alerting/state/query_part';

const index: any[] = [];
const categories: any = {
  Aggregations: [],
  Selectors: [],
  Transformations: [],
  Predictors: [],
  Math: [],
  Aliasing: [],
  Fields: [],
};

function createPart(part: any): any {
  const def = index[part.type];
  if (!def) {
    throw { message: 'Could not find query part ' + part.type };
  }

  return new QueryPart(part, def);
}

function register(options: any) {
  index[options.type] = new QueryPartDef(options);
  options.category.push(index[options.type]);
}

const groupByTimeFunctions: any[] = [];

function aliasRenderer(part: { params: string[] }, innerExpr: string) {
  return innerExpr + ' AS ' + '"' + part.params[0] + '"';
}

function fieldRenderer(part: { params: string[] }) {
  const param = part.params[0];

  if (param === '*') {
    return '*';
  }

  let escapedParam = `"${param}"`;

  if (param.endsWith('::tag')) {
    escapedParam = `"${param.slice(0, -5)}"::tag`;
  }

  if (param.endsWith('::field')) {
    escapedParam = `"${param.slice(0, -7)}"::field`;
  }

  return escapedParam;
}

function replaceAggregationAddStrategy(selectParts: any[], partModel: { def: { type: string } }) {
  // look for existing aggregation
  for (let i = 0; i < selectParts.length; i++) {
    const part = selectParts[i];
    if (part.def.category === categories.Aggregations) {
      if (part.def.type === partModel.def.type) {
        return;
      }
      // count distinct is allowed
      if (part.def.type === 'count' && partModel.def.type === 'distinct') {
        break;
      }
      // remove next aggregation if distinct was replaced
      if (part.def.type === 'distinct') {
        const morePartsAvailable = selectParts.length >= i + 2;
        if (partModel.def.type !== 'count' && morePartsAvailable) {
          const nextPart = selectParts[i + 1];
          if (nextPart.def.category === categories.Aggregations) {
            selectParts.splice(i + 1, 1);
          }
        } else if (partModel.def.type === 'count') {
          if (!morePartsAvailable || selectParts[i + 1].def.type !== 'count') {
            selectParts.splice(i + 1, 0, partModel);
          }
          return;
        }
      }
      selectParts[i] = partModel;
      return;
    }
    if (part.def.category === categories.Selectors) {
      selectParts[i] = partModel;
      return;
    }
  }

  selectParts.splice(1, 0, partModel);
}

function addTransformationStrategy(selectParts: any[], partModel: any) {
  let i;
  // look for index to add transformation
  for (i = 0; i < selectParts.length; i++) {
    const part = selectParts[i];
    if (part.def.category === categories.Math || part.def.category === categories.Aliasing) {
      break;
    }
  }

  selectParts.splice(i, 0, partModel);
}

function addMathStrategy(selectParts: any[], partModel: any) {
  const partCount = selectParts.length;
  if (partCount > 0) {
    // if last is math, replace it
    if (selectParts[partCount - 1].def.type === 'math') {
      selectParts[partCount - 1] = partModel;
      return;
    }
    // if next to last is math, replace it
    if (partCount > 1 && selectParts[partCount - 2].def.type === 'math') {
      selectParts[partCount - 2] = partModel;
      return;
    } else if (selectParts[partCount - 1].def.type === 'alias') {
      // if last is alias add it before
      selectParts.splice(partCount - 1, 0, partModel);
      return;
    }
  }
  selectParts.push(partModel);
}

function addAliasStrategy(selectParts: any[], partModel: any) {
  const partCount = selectParts.length;
  if (partCount > 0) {
    // if last is alias, replace it
    if (selectParts[partCount - 1].def.type === 'alias') {
      selectParts[partCount - 1] = partModel;
      return;
    }
  }
  selectParts.push(partModel);
}

function addFieldStrategy(selectParts: any, partModel: any, query: { selectModels: any[][] }) {
  // copy all parts
  const parts = map(selectParts, (part: any) => {
    return createPart({ type: part.def.type, params: clone(part.params) });
  });

  query.selectModels.push(parts);
}

register({
  type: 'field',
  addStrategy: addFieldStrategy,
  category: categories.Fields,
  params: [{ type: 'field', dynamicLookup: true }],
  defaultParams: ['value'],
  renderer: fieldRenderer,
});

// Aggregations
register({
  type: 'count',
  addStrategy: replaceAggregationAddStrategy,
  category: categories.Aggregations,
  params: [],
  defaultParams: [],
  renderer: functionRenderer,
});

register({
  type: 'distinct',
  addStrategy: replaceAggregationAddStrategy,
  category: categories.Aggregations,
  params: [],
  defaultParams: [],
  renderer: functionRenderer,
});

register({
  type: 'integral',
  addStrategy: replaceAggregationAddStrategy,
  category: categories.Aggregations,
  params: [],
  defaultParams: [],
  renderer: functionRenderer,
});

register({
  type: 'mean',
  addStrategy: replaceAggregationAddStrategy,
  category: categories.Aggregations,
  params: [],
  defaultParams: [],
  renderer: functionRenderer,
});

register({
  type: 'median',
  addStrategy: replaceAggregationAddStrategy,
  category: categories.Aggregations,
  params: [],
  defaultParams: [],
  renderer: functionRenderer,
});

register({
  type: 'mode',
  addStrategy: replaceAggregationAddStrategy,
  category: categories.Aggregations,
  params: [],
  defaultParams: [],
  renderer: functionRenderer,
});

register({
  type: 'sum',
  addStrategy: replaceAggregationAddStrategy,
  category: categories.Aggregations,
  params: [],
  defaultParams: [],
  renderer: functionRenderer,
});

// transformations

register({
  type: 'derivative',
  addStrategy: addTransformationStrategy,
  category: categories.Transformations,
  params: [
    {
      name: 'duration',
      type: 'interval',
      options: ['1s', '10s', '1m', '5m', '10m', '15m', '1h'],
    },
  ],
  defaultParams: ['10s'],
  renderer: functionRenderer,
});

register({
  type: 'spread',
  addStrategy: addTransformationStrategy,
  category: categories.Transformations,
  params: [],
  defaultParams: [],
  renderer: functionRenderer,
});

register({
  type: 'non_negative_derivative',
  addStrategy: addTransformationStrategy,
  category: categories.Transformations,
  params: [
    {
      name: 'duration',
      type: 'interval',
      options: ['1s', '10s', '1m', '5m', '10m', '15m', '1h'],
    },
  ],
  defaultParams: ['10s'],
  renderer: functionRenderer,
});

register({
  type: 'difference',
  addStrategy: addTransformationStrategy,
  category: categories.Transformations,
  params: [],
  defaultParams: [],
  renderer: functionRenderer,
});

register({
  type: 'non_negative_difference',
  addStrategy: addTransformationStrategy,
  category: categories.Transformations,
  params: [],
  defaultParams: [],
  renderer: functionRenderer,
});

register({
  type: 'moving_average',
  addStrategy: addTransformationStrategy,
  category: categories.Transformations,
  params: [{ name: 'window', type: 'int', options: [5, 10, 20, 30, 40] }],
  defaultParams: [10],
  renderer: functionRenderer,
});

register({
  type: 'cumulative_sum',
  addStrategy: addTransformationStrategy,
  category: categories.Transformations,
  params: [],
  defaultParams: [],
  renderer: functionRenderer,
});

register({
  type: 'stddev',
  addStrategy: addTransformationStrategy,
  category: categories.Transformations,
  params: [],
  defaultParams: [],
  renderer: functionRenderer,
});

register({
  type: 'time',
  category: groupByTimeFunctions,
  params: [
    {
      name: 'interval',
      type: 'time',
      options: ['$__interval', '1s', '10s', '1m', '5m', '10m', '15m', '1h'],
    },
  ],
  defaultParams: ['$__interval'],
  renderer: functionRenderer,
});

register({
  type: 'fill',
  category: groupByTimeFunctions,
  params: [
    {
      name: 'fill',
      type: 'string',
      options: ['none', 'null', '0', 'previous', 'linear'],
    },
  ],
  defaultParams: ['null'],
  renderer: functionRenderer,
});

register({
  type: 'elapsed',
  addStrategy: addTransformationStrategy,
  category: categories.Transformations,
  params: [
    {
      name: 'duration',
      type: 'interval',
      options: ['1s', '10s', '1m', '5m', '10m', '15m', '1h'],
    },
  ],
  defaultParams: ['10s'],
  renderer: functionRenderer,
});

// predictions
register({
  type: 'holt_winters',
  addStrategy: addTransformationStrategy,
  category: categories.Predictors,
  params: [
    { name: 'number', type: 'int', options: [5, 10, 20, 30, 40] },
    { name: 'season', type: 'int', options: [0, 1, 2, 5, 10] },
  ],
  defaultParams: [10, 2],
  renderer: functionRenderer,
});

register({
  type: 'holt_winters_with_fit',
  addStrategy: addTransformationStrategy,
  category: categories.Predictors,
  params: [
    { name: 'number', type: 'int', options: [5, 10, 20, 30, 40] },
    { name: 'season', type: 'int', options: [0, 1, 2, 5, 10] },
  ],
  defaultParams: [10, 2],
  renderer: functionRenderer,
});

// Selectors
register({
  type: 'bottom',
  addStrategy: replaceAggregationAddStrategy,
  category: categories.Selectors,
  params: [{ name: 'count', type: 'int' }],
  defaultParams: [3],
  renderer: functionRenderer,
});

register({
  type: 'first',
  addStrategy: replaceAggregationAddStrategy,
  category: categories.Selectors,
  params: [],
  defaultParams: [],
  renderer: functionRenderer,
});

register({
  type: 'last',
  addStrategy: replaceAggregationAddStrategy,
  category: categories.Selectors,
  params: [],
  defaultParams: [],
  renderer: functionRenderer,
});

register({
  type: 'max',
  addStrategy: replaceAggregationAddStrategy,
  category: categories.Selectors,
  params: [],
  defaultParams: [],
  renderer: functionRenderer,
});

register({
  type: 'min',
  addStrategy: replaceAggregationAddStrategy,
  category: categories.Selectors,
  params: [],
  defaultParams: [],
  renderer: functionRenderer,
});

register({
  type: 'percentile',
  addStrategy: replaceAggregationAddStrategy,
  category: categories.Selectors,
  params: [{ name: 'nth', type: 'int' }],
  defaultParams: [95],
  renderer: functionRenderer,
});

register({
  type: 'top',
  addStrategy: replaceAggregationAddStrategy,
  category: categories.Selectors,
  params: [{ name: 'count', type: 'int' }],
  defaultParams: [3],
  renderer: functionRenderer,
});

register({
  type: 'tag',
  category: groupByTimeFunctions,
  params: [{ name: 'tag', type: 'string', dynamicLookup: true }],
  defaultParams: ['tag'],
  renderer: fieldRenderer,
});

register({
  type: 'math',
  addStrategy: addMathStrategy,
  category: categories.Math,
  params: [{ name: 'expr', type: 'string' }],
  defaultParams: [' / 100'],
  renderer: suffixRenderer,
});

register({
  type: 'alias',
  addStrategy: addAliasStrategy,
  category: categories.Aliasing,
  params: [{ name: 'name', type: 'string', quote: 'double' }],
  defaultParams: ['alias'],
  renderMode: 'suffix',
  renderer: aliasRenderer,
});

export default {
  create: createPart,
  getCategories: () => {
    return categories;
  },
  replaceAggregationAdd: replaceAggregationAddStrategy,
};
