import {
  DataQueryRequest,
  DataQueryResponse,
  DataSourceApi,
  DataSourceInstanceSettings,
  MutableDataFrame,
  MetricFindValue,
  FieldType,
  DataQueryError,
} from '@grafana/data';

import ResponseParser from './response_parser';
import KDBQuery from './kdb_query';
import { MyQuery, MyDataSourceOptions } from './types';
import { tabFunction, defaultTimeout, kdbEpoch, durationMap } from './model/kdb-request-config';
import { KdbRequest } from './model/kdb-request';
import { QueryParam } from './model/query-param';
import { C } from './c';
import { QueryDictionary } from './model/queryDictionary';
import { ConflationParams } from './model/conflationParams';
import { graphFunction } from './model/kdb-request-config';
import { conflationUnitDefault } from './QueryEditor';
import _ from 'lodash';
import { getTemplateSrv } from '@grafana/runtime';

export class DataSource extends DataSourceApi<MyQuery, MyDataSourceOptions> {
  //This is declaring the types of each member
  id: any;
  name: any;
  variables: any;
  responseParser: ResponseParser;
  queryModel: KDBQuery;
  //  interval: string;
  message = {};
  url: string;
  wsUrl: string;
  ws: WebSocket;
  awaitingResponse: boolean;
  c: C = new C();
  maxRowCount: number;
  connectionStateCycles: number;
  timeoutLength: number;

  //WebSocket communication variables
  requestSentList: any[];
  requestSentIDList: any[];
  responseReceivedList: any[];

  constructor(
    instanceSettings: DataSourceInstanceSettings<MyDataSourceOptions>,
    private backendSrv,
    private $q,
    private templateSrv
  ) {
    super(instanceSettings);

    this.templateSrv = templateSrv;
    this.name = instanceSettings.name;
    this.id = instanceSettings.id;
    this.responseParser = new ResponseParser(this.$q);
    this.queryModel = new KDBQuery({});
    this.interval = (instanceSettings.jsonData || {}).timeInterval;
    if (!instanceSettings.jsonData.timeoutLength) {
      this.timeoutLength = defaultTimeout;
    } else {
      this.timeoutLength = Number(instanceSettings.jsonData.timeoutLength);
    }
    this.requestSentList = [];
    this.requestSentIDList = [];
    this.responseReceivedList = [];

    this.url = 'http://' + instanceSettings.jsonData.host;
    if (instanceSettings.jsonData.useAuthentication) {
      if (instanceSettings.jsonData.useTLS === true) {
        this.wsUrl =
          'wss://' +
          instanceSettings.jsonData.user +
          ':' +
          instanceSettings.jsonData.password +
          '@' +
          instanceSettings.jsonData.host;
      } else {
        this.wsUrl =
          'ws://' +
          instanceSettings.jsonData.user +
          ':' +
          instanceSettings.jsonData.password +
          '@' +
          instanceSettings.jsonData.host;
      }
    } else {
      this.wsUrl = 'ws://' + instanceSettings.jsonData.host;
    }
  }

  query(options: DataQueryRequest<MyQuery>): Promise<DataQueryResponse> {
    var prefilterResultCount = options.targets.length;

    if (prefilterResultCount == 0) {
      return new Promise((resolve) => {
        resolve({ data: [] });
      });
    }

    var allRefIDs = [];
    var blankRefIDs = [];
    var validRequestList = [];
    var errorList = [];

    for (var i = 0; i < prefilterResultCount; i++) {
      //Inject variables into target
      this.injectVariables(options.targets[i], options.scopedVars, options.range);

      // for some reason randomWalk is defaulted
      if (options.targets[i].queryType == 'randomWalk') {
        options.targets[i].queryType = 'selectQuery';
      }
      allRefIDs.push(options.targets[i].refId);
      options.targets[i].range = options.range;
      if (
        (!options.targets[i].table && options.targets[i].queryType === 'selectQuery') ||
        (options.targets[i].queryType === 'functionQuery' && options.targets[i].kdbFunction === '') ||
        options.targets[i].hide === true
      ) {
        blankRefIDs.push(options.targets[i].refId);
      } else if (!options.targets[i].queryError) {
        blankRefIDs.push(options.targets[i].refId);
      } else if (options.targets[i].queryError.error.indexOf(true) !== -1) {
        errorList.push({
          refId: options.targets[i].refId,
          errorMessage: options.targets[i].queryError.message[options.targets[i].queryError.error.indexOf(true)],
        });
      } else validRequestList.push(options.targets[i]);
    }

    var nrBlankRequests = blankRefIDs.length;
    var requestList = validRequestList.map((target) => {
      return this.buildKdbRequest(target);
    });

    var nrRequests: number = requestList.length;
    if (!this.ws || this.ws.readyState > 1)
      return this.connectWS().then((connectStatus) => {
        if (connectStatus === true && nrRequests > 0)
          return this.sendQueries(nrRequests, requestList, nrBlankRequests, blankRefIDs, errorList).then(
            (series: any) => {
              return this.buildDataFrames(series);
            }
          );
        else if (connectStatus === true && nrRequests === 0)
          return this.emptyQueries(nrBlankRequests, blankRefIDs, errorList).then(() => {
            return { data: [] };
          });
        else
          return this.connectFail(prefilterResultCount, allRefIDs).then(() => {
            return { data: [] };
          });
      });
    else {
      return this.webSocketWait().then(() => {
        if (nrRequests > 0)
          return this.sendQueries(nrRequests, requestList, nrBlankRequests, blankRefIDs, errorList).then(
            (series: any) => {
              return this.buildDataFrames(series);
            }
          );
        else
          return this.emptyQueries(nrBlankRequests, blankRefIDs, errorList).then(() => {
            return { data: [] };
          });
      });
    }
  }

  // Build the DataFrames to return to Grafana
  buildDataFrames(series) {
    let data: MutableDataFrame[] = [];
    let error = {} as DataQueryError;

    series.data.forEach((target) => {
      if (target.meta.errorMessage) {
        error.message = target.meta.errorMessage;
        throw new Error(target.meta.errorMessage);
      }
      if (target.columns) {
        var fields = [];
        target.columns.forEach((column) => {
          fields.push({ name: column.text });
        });
        const frame = new MutableDataFrame({
          refId: target.refId,
          fields: fields,
        });
        target.rows.forEach((element) => {
          var row = [];
          element.forEach((entry) => {
            row.push(entry);
          });
          frame.appendRow(row);
        });
        data.push(frame);
      } else {
        // time series

        let datapoints = target.datapoints;
        const timeValues = datapoints.map((datapoint) => datapoint[1]);
        const values = datapoints.map((datapoint) => datapoint[0]);

        const fields = [
          { name: 'Time', values: timeValues, type: FieldType.time },
          {
            name: target.target,
            values: values,
          },
        ];

        data.push(
          new MutableDataFrame({
            refId: target.refId,
            fields: fields,
          })
        );
      }
    });
    return { data, state: 'done' };
  }

  //This is the function called by Grafana when it is testing a connection on the configuration page
  testDatasource() {
    return this.connect().then((result) => {
      return result;
    });
  }
  //Replace variables with their values
  private variablesReplace(target: any, search: string, replace: any) {
    //Format Options as array or scalar
    if (Array.isArray(replace)) {
      target.kdbFunction = target.kdbFunction.replace(search, replace.join(','));
    } else {
      target.kdbFunction = target.kdbFunction.replace(search, replace);
    }
    //Replace Table Variables
    target.table = this.fieldInjectVariables(target.table, search, replace);
    //Replace select clause variables
    if (target.select !== [] && target.select.length > 0) {
      for (let i = 0; i < target.select[0].length; i++) {
        for (let y = 0; y < target.select[0][i].params.length; y++) {
          target.select[0][i].params[y] = target.select[0][i].params[y].replace(search, replace);
        }
      }
    }
    //Replace where clause variables
    if (target.where !== []) {
      for (let i = 0; i < target.where.length; i++) {
        for (let y = 0; y < target.where[i].params.length; y++) {
          if (Array.isArray(replace) && replace.length > 1) {
            if (target.where[i].params[y] === search) target.where[i].params[y] = replace;
          } else if ('string' == typeof target.where[i].params[y]) {
            target.where[i].params[y] = target.where[i].params[y].replace(search, replace);
          }
        }
      }
    }
    //Replace time, grouping and funcGroup columns if required
    target.timeColumn = this.fieldInjectVariables(target.timeColumn, search, replace);
    target.groupingField = this.fieldInjectVariables(target.groupingField, search, replace);
    target.funcGroupCol = this.fieldInjectVariables(target.funcGroupCol, search, replace);
    //Check row count is formatted correctly
    if ('string' == typeof target.rowCountLimit) {
      if (target.rowCountLimit === search) {
        if (Number.isInteger(Number(replace)) && Number(replace) > 0) {
          target.rowCountLimit = Number(replace);
        } else {
          target.queryError.error[2] = true;
          target.queryError.message[2] = 'Row count limit not a positive integer';
        }
      }
    }
    //Check conflation params are formatted correctly
    if ('string' == typeof target.conflationDuration) {
      if (target.conflationDuration === search) {
        if (isNaN(Number(replace))) {
          target.queryError.error[1] = true;
          target.queryError.message[1] = 'Conflation duration not a number';
        } else {
          target.conflationDuration = Number(replace);
        }
      }
    }
  }

  //Check if attribute needs replacing, then replace if so
  private fieldInjectVariables(attrib: any, search: string, replace: any) {
    // console.log('s',search)
    // console.log('r',replace)
    if (attrib) {
      attrib = attrib.replace(search, replace);
      // console.log('a1',attrib)
      return attrib;
    } else {
      // console.log('a2',attrib)
      return attrib;
    }
  }

  private injectVariables(target, scoped, range) {
    let instVariables = this.newGetVariables(getTemplateSrv());
    // console.log('TEMPLATESRV:', this.templateSrv);
    // console.log('VARIABLES: ', instVariables);
    // console.log('scp',scoped)
    let scopedVarArray = Object.keys(scoped);
    let scopedValueArray = [];
    //scoped variables inject
    for (let k = 0; k < scopedVarArray.length; k++) {
      scopedValueArray.push(scoped[scopedVarArray[k]].value);
      scopedVarArray[k] = '$' + scopedVarArray[k];
    }
    //local variables inject (user variables)
    for (let i = 0; i < instVariables.length; i++) {
      let varname = '${' + instVariables[i].name + '}';

      // console.log(varname.length)
      // console.log('vname:',varname)
      if (scopedVarArray.indexOf(varname) == -1) {
        scopedVarArray.push(varname);
        if (instVariables[i].current.text === 'All') {
          // console.log('trig1')
          scopedValueArray.push(instVariables[i].allValue);
        } else {
          // console.log('trig2')
          scopedValueArray.push(instVariables[i].current.value);
        }
      }
    }
    // console.log('scopedval',scopedValueArray)
    //$__from & $__to inject
    scopedVarArray.push('${__from}');
    scopedValueArray.push('(`timestamp$' + this.buildKdbTimestamp(range.from._d) + ')');
    scopedVarArray.push('${__to}');
    scopedValueArray.push('(`timestamp$' + this.buildKdbTimestamp(range.to._d) + ')');
    //Replace variables
    // console.log('TARGET: ',target);
    // console.log('SCOPEDVARARRAY:', scopedVarArray);
    // console.log('SCOPEDVALUEARRAY:', scopedValueArray);
    for (let kv = 0; kv < scopedVarArray.length; kv++) {
      this.variablesReplace(target, scopedVarArray[kv], scopedValueArray[kv]);
    }
  }

  //Change templateSrv object to be handled as variables
  private newGetVariables(templatesrv) {
    let instVariables = [];
    let variables = JSON.parse(JSON.stringify(templatesrv.getVariables()));
    for (let i = 0; i < variables.length; i++) {
      //Set the 'all' value if the option is enabled
      if (variables[i].options[0] && variables[i].options[0].text === 'All') {
        let valueArray = [];
        for (let j = 1; j < variables[i].options.length; j++) {
          valueArray.push(variables[i].options[j].value);
        }
        variables[i].allValue = valueArray;
      }
      instVariables.push(variables[i]);
    }
    return instVariables;
  }

  //Websocket per request?
  private buildKdbRequest(target) {
    let queryParam = new QueryParam();
    let kdbRequest = new KdbRequest();
    let queryDictionary = new QueryDictionary();
    let conflationParams = new ConflationParams();

    //Need to take into account quotes in line, replace " with \"
    queryDictionary.type = target.queryType == 'selectQuery' ? '`select' : '`function';
    queryDictionary.value = target.kdbFunction;

    queryParam.query = Object.assign({}, queryDictionary);
    queryParam.table = '`' + target.table;
    queryParam.column = this.buildColumnParams(target);
    queryParam.temporal_field = target.useTemporalField ? this.buildTemporalField(target) : [];
    queryParam.temporal_range = this.buildTemporalRange(target.range);
    queryParam.maxRowCount = target.rowCountLimit;
    // if (target.postbackFunction) queryParam.postbackFunction = target.postbackFunction;

    if (target.queryType == 'selectQuery') queryParam.where = this.buildWhereParams(target.where ? target.where : []);
    //conflation
    if (target.useConflation) {
      this.buildConflation(target);
      conflationParams.val = target.conflationDurationMS.toString();
      conflationParams.agg = target.conflation.aggregate;
      queryParam.conflation = Object.assign({}, conflationParams);
    } else {
      queryParam.conflation = [];
    }

    //add condition, has grouping been selected?
    if (target.useGrouping && target.queryType == 'selectQuery' && target.groupingField) {
      queryParam.grouping = ['`' + target.groupingField];
    } else if (target.useGrouping && target.queryType == 'functionQuery' && target.funcGroupCol) {
      queryParam.grouping = ['`' + target.funcGroupCol];
    } else {
      queryParam.grouping = [];
    }

    kdbRequest.time = this.getTimeStamp(new Date());
    kdbRequest.refId = target.refId;
    kdbRequest.query = ''; //query;
    kdbRequest.queryParam = Object.assign({}, queryParam);
    kdbRequest.format = target.format;
    kdbRequest.queryId = target.queryId;
    kdbRequest.version = target.version;
    // if (target.useAsyncFunction) kdbRequest.useAsyncFunction = target.useAsyncFunction;
    // if (target.useCustomPostback) kdbRequest.useCustomPostback = target.useCustomPostback;
    // if (target.asyncProcTypes) kdbRequest.asyncProcTypes = target.asyncProcTypes;

    for (let i = 0; i < target.queryError.error.length; i++) {
      if (target.queryError.error[i]) {
        throw new Error(target.queryError.message[i]);
      }
    }

    return [target.format == 'time series' ? graphFunction : tabFunction, Object.assign({}, kdbRequest)];
  }

  //This function
  private buildTemporalField(queryDetails) {
    if (queryDetails.queryType == 'selectQuery' && queryDetails.timeColumn) {
      return '`' + queryDetails.timeColumn;
    } else if (queryDetails.queryType == 'functionQuery' && queryDetails.funcTimeCol) {
      return '`' + queryDetails.funcTimeCol;
    } else {
      return [];
    }
  }

  private buildConflation(queryDetails) {
    if (['s', 'm', 'h', 'ms'].indexOf(queryDetails.conflation.unitType) == -1) {
      queryDetails.conflationUnit = conflationUnitDefault;
      queryDetails.queryError.error[1] = true;
      queryDetails.queryError.message[1] =
        'Conflation unit not support. Please post conflation settings on our GitHub page.';
    }
    queryDetails.conflationDurationMS =
      queryDetails.conflation.duration * durationMap[queryDetails.conflation.unitType];
  }

  private buildKdbTimestamp(date: Date) {
    return 1000000 * (date.valueOf() - kdbEpoch);
  }

  //Getting it to work via strings would require supporting timezones fully. Rabbit hole.
  /* private ES2015padStart(obj: string, length: number, fill: string) {
    //Effectively polyfill for String.padStart (fill length will only fill up to 10 missing characters)
    let f = length - obj.length;
    return f > 0 ? fill.repeat(10).substr(0,f) + obj : obj
}

private buildKdbTimestampString(date : Date) {
    let dt = date.getFullYear().toString() + '.' + 
        this.ES2015padStart((date.getMonth() + 1).toString(), 2, "0") + '.' + 
        this.ES2015padStart(date.getDate().toString(), 2, "0");
    let tm = this.ES2015padStart(date.getHours().toString(), 2, "0") + ':' + 
        this.ES2015padStart(date.getMinutes().toString(), 2, "0") + ':' + 
        this.ES2015padStart(date.getSeconds().toString(), 2, "0") + '.' + 
        this.ES2015padStart(date.getMilliseconds().toString(), 3, "0");
    return dt + 'D' + tm;
} */

  private buildTemporalRange(range) {
    let temporalRange: number[] = [];
    if (range) {
      temporalRange.push(this.buildKdbTimestamp(range.from._d));
      temporalRange.push(this.buildKdbTimestamp(range.to._d));
    }
    return temporalRange;
  }

  private buildWhereParams(queryWhereList): Array<string> {
    let whereArray = [];
    let whereClause = [];

    if (queryWhereList.length > 0) {
      queryWhereList.forEach((clause) => {
        let notStatement = false;
        if (clause.params[0] !== 'select field' && clause.params[2] !== 'enter value') {
          whereClause = [];
          if (clause.params[1].substr(0, 3) == 'not') {
            clause.params[1] = clause.params[1].substr(4);
            whereClause.push(clause.params[1]);
            notStatement = true;
          } else whereClause.push(clause.params[1]);
          whereClause.push('`' + clause.params[0]);
          //                    if (clause.datatype == 's') {
          if (['in', 'within'].indexOf(clause.params[1]) != -1) {
            if ('string' == typeof clause.params[2]) {
              whereClause.push(clause.params[2].split(',').map((str) => str.trim()));
            } else {
              whereClause.push(clause.params[2]);
            }
          } else if (clause.params[1] == 'like') {
            whereClause.push('"' + clause.params[2] + '"');
          } else whereClause.push(clause.params[2]);
          //                    }
          //                    else if (clause.datatype == 'c') {
          //                        whereClause.push('\"' + clause.params[2] + '\"');
          //                    }
          //                    else {
          //                        if (clause.params[1] == "within") {
          //                            whereClause.push(clause.params[2].split(",").map(str => str.trim()))
          //                        } else whereClause.push(clause.params[2]);
          //                    }
          if (notStatement === true) {
            // console.log('WHERECLAUSE', whereClause)
            whereClause.push('x');
          } else whereClause.push('o');
          whereArray.push(whereClause);
        }
      });
    }

    return whereArray;
  } //end of building of where clause

  //Builds the list of select functions consisting of the column name and an aggregation function where applicable
  private buildColumnParams(target): Array<string> {
    let columnArray: any[] = [];

    if (target.select) {
      target.select.forEach((select) => {
        if (select[0].params[0] !== 'select column') {
          let selectElement = [];
          if (target.useConflation) {
            if (select.length !== 1) {
              if (select[1].type == 'aggregate') {
                selectElement.push(select[1].params[0]);
              } else {
                selectElement.push(target.conflation.aggregate);
              }
            } else {
              selectElement.push(target.conflation.aggregate);
            }
          } else {
            selectElement.push('::'); //dummy value for kdb function
          }
          selectElement.push('`' + select[0].params[0]);

          //dealing with aliasing
          let alias = '::';
          if (select.length > 1) {
            if (select[1].type == 'alias') {
              alias = select[1].params[0];
            }
          }
          if (select.length == 3) {
            if (select[2].type == 'alias') {
              alias = select[2].params[0];
            }
          }

          selectElement.push(alias);
          columnArray.push(selectElement);
        }
      });
    } else {
      //temporary fix
      columnArray.push(['::', '`value', '::']);
    }
    return columnArray;
  }

  private getTimeStamp(date: Date): string {
    let dateString = date.valueOf().toString();
    return dateString.substring(0, dateString.length - 3);
  }

  showEmpty(Id: string, errormessage?: string) {
    if (typeof errormessage === 'undefined') {
      var returnobj = {
        refId: Id,
        columns: [],
        rows: [],
        meta: { refId: Id, errorReceived: false, errorMessage: '' },
      };
    } else {
      var returnobj = {
        refId: Id,
        columns: [],
        rows: [],
        meta: { refId: Id, errorReceived: true, errorMessage: errormessage },
      };
    }
    return returnobj;
  }

  errorReturn(errorstring: string) {
    return { payload: [], error: errorstring, success: false };
  }

  sendQueries(nrRequests, requestList, nrBlankRequests, blankRefIDs, errorList) {
    var curRequest: number = 0;
    var resultList = [];

    return new Promise((resolve) => {
      this.ProcessData(curRequest, nrRequests, resultList, requestList)
        .then(() => {
          for (var i = 0; i < nrBlankRequests; i++) {
            resultList.push(this.showEmpty(blankRefIDs[i]));
          }
          for (var i = 0; i < errorList.length; i++) {
            resultList.push(this.showEmpty(errorList[i].refId, errorList[i].errorMessage));
          }
          resolve({ data: resultList });
        })
        .catch((e) => {});
    });
  }
  connectFail(prefilterResultCount, allRefIDs) {
    return new Promise((resolve) => {
      let serverUnavailableResponse = [];
      for (var i = 0; i < prefilterResultCount; i++) {
        serverUnavailableResponse.push(this.showEmpty(allRefIDs[i], 'KDB+ server unavailable.'));
      }
      resolve({ data: serverUnavailableResponse });
    });
  }

  emptyQueries(nrBlankRequests, blankRefIDs, errorList) {
    return new Promise((resolve) => {
      let resultList = [];
      for (var i = 0; i < nrBlankRequests; i++) {
        resultList.push(this.showEmpty(blankRefIDs[i]));
      }
      for (var i = 0; i < errorList.length; i++) {
        resultList.push(this.showEmpty(errorList[i].refId, errorList[i].errorMessage));
      }
      resolve({ data: resultList });
    });
  }

  private ProcessData(curRequest, nrRequests, resultList, requestList) {
    return new Promise((resolve) => {
      this.getQueryResult(requestList[curRequest]).then((result) => {
        var indicies = Object.keys(result);
        if (result.hasOwnProperty('meta.errorReceived')) {
          resultList.push(result);
        } else {
          for (let i = 0; i < indicies.length; i++) {
            resultList.push(result[i]);
          }
        }

        if (curRequest == nrRequests - 1) {
          let returnVal = resultList;
          resolve(returnVal);
        } else {
          curRequest++;
          resolve(this.ProcessData(curRequest, nrRequests, resultList, requestList));
        }
      });
    });
  }

  //Response parser called here**********************
  private getQueryResult = (request: any): Promise<Object> => {
    let curRequest = request;
    let timeoutError = 'Query sent at ' + new Date() + ' timed out.';
    let malformedResError = 'Malformed response. Check KDB+ WebSocket handler is correctly configured.';
    let response = new Promise((resolve) => {
      this.executeAsyncQuery(curRequest).then((result) => {
        {
          const processedResult = this.responseParser.processQueryResult(result, curRequest);
          if (Object.keys(result).indexOf('payload') === -1) {
            return resolve([this.showEmpty(curRequest[1].refId, malformedResError)]);
          } else return resolve(processedResult);
        }
      });
    });
    let timeout = new Promise((resolve) => {
      let wait = setTimeout(() => {
        clearTimeout(wait);
        resolve([this.showEmpty(curRequest[1].refId, timeoutError)]);
      }, this.timeoutLength);
    });
    return Promise.race([timeout, response]);
  };

  connectWS() {
    return new Promise((connected) => {
      this.ws = new WebSocket(this.wsUrl);
      this.ws.binaryType = 'arraybuffer';
      this.ws.onmessage = (response) => {
        this.executeAsyncReceive(response);
      };

      this.ws.onopen = () => {
        connected(true);
      };

      this.ws.onclose = () => {
        connected(false);
      };

      this.ws.onerror = () => {};
    });
  }

  webSocketWait() {
    return new Promise((ready) => {
      if (this.ws.readyState === 0) {
        setTimeout(() => ready(this.webSocketWait()), 20);
      } else ready('');
    });
  }

  executeAsyncQuery(request: any) {
    var requestResolve;
    let _c = this.c;
    var requestPromise = new Promise((resolve) => {
      let refIDn = Math.round(10000000 * Math.random());
      var wrappedRequest = { i: request, ID: refIDn };
      this.ws.send(_c.serialize(wrappedRequest));
      this.requestSentIDList.push(refIDn);
      requestResolve = resolve;
    });

    Object.assign(requestPromise, { resolve: requestResolve });
    let countSentList = this.requestSentList.length;
    this.requestSentList.push(requestPromise);
    return this.requestSentList[countSentList];
  }

  executeAsyncReceive(responseObj) {
    let _c = this.c;
    let deserializedResult = _c.deserialize(responseObj.data);
    if (!deserializedResult.ID) {
      // return console.log('received malformed data')
    } else if (this.requestSentIDList.indexOf(deserializedResult.ID) === -1) {
      // return console.log('received unrequested data');
    } else {
      var requestNum = this.requestSentIDList.indexOf(deserializedResult.ID);
      this.requestSentList[requestNum].resolve(deserializedResult.o);
    }
  }

  //Called for query variables
  metricFindQuery(kdbRequest: KdbRequest): Promise<MetricFindValue[]> {
    kdbRequest = this.injectUserVars(kdbRequest);
    return new Promise((resolve, reject) => {
      return this.connectWS().then((connectStatus) => {
        if (connectStatus === true){
            resolve(
              this.executeAsyncQuery(kdbRequest).then((result) => {
                const values = [];
                var properties = [];
                if (Array.isArray(result)) {
                  if (typeof result[0] === 'string') {
                    for (let i = 0; i < result.length; i++) {
                      values.push({ text: result[i] });
                    }
                  } else if (typeof result[0] === 'object') {
                    if (Object.keys(result[0]).length > 1) {
                      //checking that multiple rows for multiple columns don't come back as the preview tab only shows single values (not objects)
                      const errorResponse =
                        'Can only select single values. Attempted to return an object of key-value pairs. Unsafe query';
                      throw new Error(errorResponse);
                    }
                    for (var key in result[0]) {
                      if (result[0].hasOwnProperty(key) && typeof result[0][key] !== 'function') {
                        properties.push(key);
                      }
                    }
                    for (let i = 0; i < result.length; i++) {
                      values.push({ text: result[i][properties[0]] });
                    }
                  }
                } else if (typeof result === 'string') {
                  const errorResponse = `Check Query. Syntax error with: [ ${result} ]`;
                  throw new Error(errorResponse);
                }
                return values;
              })
            );
          }else{
            resolve([]);
          }
      });
    });
  }

  // Possibly not needed? Can reuse injectVariables instead possibly (no scoped/ranged vars though)
  injectUserVars(kdbRequest) {
    let instVariables = this.newGetVariables(getTemplateSrv());
    let localVarArray = [];
    let localValueArray = [];
    //local variables inject (user variables)
    for (let i = 0; i < instVariables.length; i++) {
      localVarArray.push('$' + instVariables[i].name);

      // Make sure length of current is greater than 0, otherwise an option hasn't been selected yet
      if (Object.keys(instVariables[i].current).length > 0 && instVariables[i].current.text[0] === 'All') {
        localValueArray.push(instVariables[i].allValue);
      } else {
        localValueArray.push(instVariables[i].current.value);
      }
    }
    for (let kv = 0; kv < localVarArray.length; kv++) {
      kdbRequest = kdbRequest.replace(localVarArray[kv], localValueArray[kv]);
    }
    return kdbRequest;
  }

  metricFindQueryDefault(/*kdbRequest: KdbRequest*/ kdbRequest: any) {
    // console.log('met',kdbRequest)
    return new Promise((resolve, reject) => {
      resolve(
        this.executeAsyncQuery(kdbRequest).then((result) => {
          return result;
        })
      );
    });
  }

  //Called for dropdowns of type s
  metricFindQuerySym(/*kdbRequest: KdbRequest*/ kdbRequest: any) {
    // console.log('met',kdbRequest)
    return new Promise((resolve, reject) => {
      resolve(
        this.executeAsyncQuery(kdbRequest).then((result) => {
          // console.log('res',result)
          let properties = [];
          for (var key in result[0]) {
            if (result[0].hasOwnProperty(key) && typeof result[0][key] !== 'function') {
              properties.push(key);
            }
          }
          for (let i = 0; i < result.length; i++) {
            result[i][properties[0]] = '`' + result[i][properties[0]];
          }

          return result;
        })
      );
    });
  }

  connect(): Promise<Object> {
    return new Promise<Object>((resolve, reject) => {
      if ('WebSocket' in window) {
        this.$q.when(this.setupWebSocket()).then(
          setTimeout(() => {
            resolve(
              this.checkConnectionState().then((result) => {
                //clearTimeout;
                return result;
              })
            );
          }, 2000)
        );
      } else {
        resolve(this.buildResponse('Error', 'WebSocket not supported!', 'Error'));
      }
    });
  }

  //This checks the kdb+ connection state for the 'test connection' funciton
  checkConnectionState(): Promise<Object> {
    return new Promise((resolve) => {
      return this.connectWS().then((connectStatus) => {
        if (connectStatus === false) {
          resolve(
            this.buildResponse(
              'fail',
              'Data source cannot be connected, if using authentication/TLS check settings as above.',
              'Fail'
            )
          );
        } else {
          let timeout = new Promise((resolve) => {
            let wait = setTimeout(() => {
              clearTimeout(wait);
              resolve(
                this.buildResponse(
                  'fail',
                  "Web socket connections aren't configured correctly for Grafana on your kdb+ instance.  Please speak to your system administrator.",
                  'Fail'
                )
              );
            }, this.timeoutLength);
          });
          let response = new Promise((resolve) => {
            this.executeAsyncQuery('.z.ws').then((res) => {
              if (typeof res !== 'string') {
                resolve(
                  this.buildResponse(
                    'fail',
                    'Malformed response. Check KDB+ WebSocket handler is correctly configured.',
                    'Fail'
                  )
                );
              } else if (res.replace(' ', '').includes('ds:-9!x;')) {
                //if it looks like .z.ws is correctly configured then return success
                resolve(this.buildResponse('success', 'Data source successfully connected!', 'Success'));
              } else {
                //If .z.ws hasn't been configured correctly on the database then return an error message
                resolve(
                  this.buildResponse(
                    'fail',
                    "Web socket connections aren't configured correctly for Grafana on your kdb+ instance.  Please speak to your system administrator.",
                    'Fail'
                  )
                );
              }
            });
          });
          return resolve(Promise.race([timeout, response]));
        }
      });
    });
  }

  setupWebSocket() {
    this.ws = new WebSocket(this.wsUrl);
    this.ws.binaryType = 'arraybuffer';

    this.ws.onopen = () => {};

    this.ws.onmessage = (messageEvent: MessageEvent) => {};

    this.ws.onclose = () => {};

    this.ws.onerror = () => {};
  }

  buildResponse(status: string, message: string, title: string) {
    return Promise.resolve({
      status,
      message,
      title,
    });
  }
}