/****** dx.0.js *******/ 

var DX = {};
DX.ModelType = {
    TEXT: 1,
    NUMBER: 2,
    DATE: 3
};
DX.Formats = {
    TEXT: "txt",
    NUMBER: "num",
    CURRENCY: "cur",
    PERCENTAGE: "pct",
    DATE: "dat2"
};
;/****** dx.datasource.js *******/ 

/* global DX:false, connector: false */
/**
 * DataSource
 */
DX.Source = $.klass({


	datasourceId : false,

	pointer : false,

	parameters : {},

	lastUpdated : false,

	updateInterval : 1000*60,


	initialize: function(dsid,pointer,params) {
		this.datasourceId = dsid;
		this.pointer = pointer;
		this.parameters = params;
	},

	getData : function () {
		return connector.getData(this);
	},


	update : function() {
//		connector.update(this);
	},

	/**
	 * not sure yet...
	 */
	destroy : function() {
		//this.connector
	},


	/**
	 * checks to see if this datasource is identical to ds2.
	 * datasources are equal if they have the same sourceId and parameters
	 * @param ds2
	 */
	equals : function( ds2 ) {
		var i;
		if ( this.datasourceId != ds2.datasourceId ) return false;
//		if ( this.pointer != ds2.pointer ) return false;
		for( i in this.parameters ) {
			if ( !this.parameters.hasOwnProperty(i) ) {
				continue;
			}
			if ( this.parameters[i] != ds2.parameters[i] ){
				return false;
			}
		}
		for( i in ds2.parameters ) {
			if( !ds2.parameters.hasOwnProperty(i) ) {
				continue;
			}
			if( ds2.parameters[i] != this.parameters[i] ){
				return false;
			}
		}
		return true;
	}
});
;/****** dx.formula.js *******/ 

/* global DX:false, isWorkspace:false */
/**
 *
 */
DX.Formula = $.klass({

	cx: null, // component which owns this formula

	id : false,
	/** cached result */
	result : false,
	/** formula string */
	formula : false,
	/** set of observed datasources */
	datasources : false,
	/** cached datasouce len */
	dsLen: 0,

	/** object representation of formula for Visual formula */
	src : false ,

	/** is the formula currently being watched by dpn for updates */
	watched : false,

	variables : false,

    /**
     * list of targets that this formula references
     * {type:"", id:""}
     */
    references : false,

	resultsReferences : false,

	dataSetId: null,

	/**
	 *
	 * @param fText
	 * @param fSrc
	 * @param component
	 * @param formulaVersion
	 */
	initialize: function(fText, fSrc, component, formulaVersion) {
        this.datasources = [];
		this.cx = component;
		this.id = "" + DX.Formula.currentFormulaIdGen++;
		this.version = formulaVersion;

		if (fText) this.setFormula(fText,fSrc);
	},

	extractDataSetId: function(formula) {
		var match;

		if (!formula) {
			return;
		}

		match = this.formula.match(/\b([a-f0-9]{32})#[a-f0-9]{32};/);
		return match && match[1] ?
			match[1] :
			null;
	},

	/**
	 *
	 * @param fText
	 * @param src
	 */
	setFormula: function(fText,src) {
		var changed = this.formula !== fText;
		this.formula = fText;
		this.src = src;

		if (fText) {
			this.dataSetId = this.extractDataSetId(this.formula);
		}

		this.datasources = [];
		if ((this.version !== DX.Formula.VERSIONS.TYPE_IN) && src) {
			this.updateDatasources(src);
		} else {
			this.updateDatasourcesFromFormula(fText);
		}

        this.initializeFormulaReferences();
		this.initializeResultsReferences();

		this.setVariables();
		return changed;
	},

	sanitizeFormulaString: function(formula) {
		//REGEX: Match a string surrounded by double quotes, ignoring escaped double quotes.
		var double_quote_regex = new RegExp(/"(?:[^"\\]|\\.)*"/); // eslint-disable-line,
		var sanitizedFormula = DX.Formula.trimComments(formula);
		sanitizedFormula = sanitizedFormula.replace(double_quote_regex,"");
		return sanitizedFormula;
	},

	setVariables: function() {
		var klip = this.cx && this.cx.getKlip();
		var formulaText = this.formula;
		var len, variableMatches;

		this._trySettingVariablesLater = false;
		if (!formulaText) {
			return;
		}

		if (this.version !== DX.Formula.VERSIONS.TYPE_IN || klip) {
			if (this.version === DX.Formula.VERSIONS.TYPE_IN) {
				formulaText = DX.Formula.replaceFormulaReferences(this.formula, klip, true);
				if (false === formulaText) {
					// failed to replace references, try again later
					this._trySettingVariablesLater = true;
					return;
				}
			}
			formulaText = this.sanitizeFormulaString(formulaText);
			if (formulaText && formulaText.indexOf("$") != -1) {
				this.variables = [];
				variableMatches = formulaText.match(/(?:\$)([a-zA-Z0-9_.]+)/g);
				if ( variableMatches ) {
					len = variableMatches.length++;
					while(--len > -1 ) {
						this.variables.push ( variableMatches[len].substr(1) );
					}
				}
			}
		} else {
			this._trySettingVariablesLater = true;
		}
	},

	initializeFormulaReferences: function() {
		if (this.version !== DX.Formula.VERSIONS.TYPE_IN && this.src) {
			this.references = [];
			this.updateReferences(this.src);
		} else if (this.cx && this.cx.getKlip()) {
			this.references = [];
			this.updateReferencesFromText(DX.Formula.FORMULA_REF);
		}
	},

	initializeResultsReferences: function() {
		if (this.version === DX.Formula.VERSIONS.TYPE_IN && this.cx && this.cx.getKlip()) {
			this.resultsReferences = [];
			if (KF.company.isPostDSTFeatureOn() && this.cx.usePostDSTReferences()) {
				this.updateReferencesFromText(DX.Formula.RESULTS_REF);
			}
		}
	},

	getFormulaReferencesArray: function() {
		if (!this.references) {
			this.initializeFormulaReferences();
		}
		return this.references;
	},

	getResultsReferencesArray: function() {
		if(!this.resultsReferences) {
			this.initializeResultsReferences();
		}
		return this.resultsReferences;
	},

	updateDatasources: function(n) {
		var dsid, nChildren;

		if (n.t == "d") {
			dsid = n.v.id;
			if ($.inArray(dsid, this.datasources) == -1) this.datasources.push(dsid);
		}
		if (!n.c) return;
		nChildren = n.c.length;
		while(--nChildren > -1) {
			this.updateDatasources(n.c[nChildren]);
		}
	},

	updateDatasourcesFromFormula: function(formula) {
		var formulaWithoutStrings = formula.replace(DX.Formula.DOUBLE_QUOTE_STRING_REGEX, "");
		var datasourceMatches = formulaWithoutStrings.match(DX.Formula.DATASOURCE_ADDRESS_REGEX);
		if (datasourceMatches) {
			datasourceMatches.forEach(function(datasourceMatch) {
				var datasourceAddress = datasourceMatch.substr(0, datasourceMatch.length - 1);
				if (-1 === this.datasources.indexOf(datasourceAddress)) {
					this.datasources.push( datasourceAddress );
				}
			}, this);
		}
	},

    updateReferences: function(n) {
		var type, id, ref, nChildren;

		if (n.t == "ref") {
            type = n.v.r;
            id = n.v[n.v.r];
            ref = {type:type, id:id};
            if (!this.referenceExists(ref)) this.references.push(ref);
        }

        if (!n.c) return;

		nChildren = n.c.length;
        while(--nChildren > -1) {
            this.updateReferences(n.c[nChildren]);
		}
    },

    updateReferencesFromText: function (refType, visited) {
        var formulaText, klip, componentReferenceMatches;
        formulaText = this.formula;
        klip = this.cx.getKlip();
        componentReferenceMatches = DX.Formula.getReferenceMatches(formulaText, refType);
        if (!visited) {
            visited = {};
        }

		componentReferenceMatches.forEach(function(componentReferenceMatch) {

			var componentId = DX.Formula.getComponentId(componentReferenceMatch.string);
			var component = klip.getComponentById(componentId);
			var referenceFormula = component && component.getFormula && component.getFormula(0);
			var referencesArray = (refType === DX.Formula.RESULTS_REF) ? this.getResultsReferencesArray() : this.getFormulaReferencesArray();

			referencesArray.push({
				type: "cx",
				id: componentId
			});

			if (referenceFormula && !visited[componentId]) {
				visited[componentId] = true;
				referenceFormula.updateReferencesFromText(refType, visited);
			}
		}, this);
	},

	getRequiredContexts: function(dstCache, variableValueOverrides) {
		//Gets the DST contexts this formula depends on directly. Note, that to correctly
		//process this formula, other DSTContexts may also be necessary if the DST the formula
		//resides in, depends on another formula with other DSTDependencies (ex: a formula that is grouped on)
		//This code does not find those DSTs, but instead relies on the caller to pass along all affected formulas

        return this.collectRelevantDSTContexts(variableValueOverrides, this.cx.getKlip(), dstCache);
	},

	getFVars: function() {
        var fVars = {}, dstVars;

        if (this._trySettingVariablesLater) {
            this.setVariables();
        }

        this.addPropsToFVars(fVars, this.variables );

        // Collect DST peer variables for data set transformation to work properly.
        dstVars = this.collectDstVariables(true);
        $.extend(fVars, dstVars);

        return fVars;
	},

	addDynamicDatasourceProperties: function(fVars, variableValueOverrides) {
        var klip = this.cx.getKlip();
        return DX.Formula.addDynamicDatasourceProperties(klip, fVars, variableValueOverrides);
	},

	addPropsToFVars: function(fVars, propNames) {
        var klip = this.cx.getKlip();
        return DX.Formula.addPropsToFVars(klip, fVars, propNames);
	},

	serialize: function(variableValueOverrides, dstCache) {
		var useCache = true;
		var formulaText = this.getFormulaText();
		var fvars;
		var dstContexts = [];
		var results;

		if (!this.cx.usePostDSTReferences()) {
            results = this.getRequiredContexts(dstCache, variableValueOverrides);
            dstContexts = results.dsts;
            fvars = results.vars;

			//Should not be an array if post-DST is not turned on. This is to ensure that PDPNs still work
			if (dstContexts && dstContexts.length > 0) {
				dstContexts = dstContexts[0]; //Should only be 1

                //Pre-Post-DST dstContext should not have an id, remove it in case we are executing a Klip
                //created with the feature on
                if (dstContexts && dstContexts.id) {
					delete dstContexts.id;
				}
			}
        }

		return {
			id: this.id,
			cid: this.cx.id,
			kid: this.cx.getKlipInstanceId(),
			klip_pid: this.cx.getKlipPublicId(),
			fm: formulaText,
			vars: fvars,
			cache: useCache,
			dst: dstContexts,
			autoFmt: this.cx.shouldSendAutoFormatToDpn(isWorkspace()),
			useNewDateFormat: this.cx.useNewDateFormat(),
            appliedMigrations: (this.cx.getKlip())? this.cx.getKlip().getAppliedMigrations() : ""
		};
	},

	collectRelevantDSTContexts: function(variableValueOverrides, klip, dstCache, dstContexts, fvars, mappedDSTIds, mappedFormulaIds) {
		var dstContext;
		var rootDSTComponent;
		var allRefs;

		//Add in the current formulas required variables
        fvars = (fvars)? fvars : {};
        $.extend(fvars,this.addDynamicDatasourceProperties(this.getFVars(),  variableValueOverrides));

		dstContexts = (dstContexts)? dstContexts : [];
        mappedDSTIds = (mappedDSTIds) ? mappedDSTIds : {}; //track which dsts we have already collected
        mappedFormulaIds = (mappedFormulaIds) ? mappedFormulaIds : {}; //Prevent recursive loops pointing to the same component

        dstContext = this.cx.getDstContextForEvaluation(fvars, dstCache);

        //With post-DST off we may not have a dstContext
        if (dstContext) {

            if (!mappedFormulaIds[this.id]) {

                //Even if post-dst is off, we still add the current component's dst
                if (!dstContext.id) {
                    //We are dealing with a klip created pre-post-dst. Add in the id from the dstRoot
                    rootDSTComponent = this.cx.getDstRootComponent();
                    if (rootDSTComponent) {
                        dstContext.id = rootDSTComponent.getDstHelper().getId();
                    }
                }

                if (dstContext.id && !mappedDSTIds[dstContext.id]) {
                    dstContexts.push(dstContext);
                    mappedDSTIds[dstContext.id] = true;
                }

                mappedFormulaIds[this.id] = true;  //Make sure we don't loop back to the same formula

                if (this.cx.usePostDSTReferences() && this.cx.getKlip().hasResultReferences()) {
                    //Now recurse over the referenced formulas, and include their DSTContexts
					//Looks at both result references and formula references since formula references may refer to result
					//references from other components.
					allRefs = [];
					if (this.resultsReferences) {
						allRefs = allRefs.concat(this.resultsReferences);
					}
					if (this.references) {
						allRefs = allRefs.concat(this.references);
					}
					allRefs.forEach(function (reference) {
						var referencedFormula;
						var component = klip.getComponentById(reference.id);
						if (component && component.hasFormula()) {
							referencedFormula = component.getFormula(0);
							referencedFormula.collectRelevantDSTContexts(variableValueOverrides, klip, dstCache, dstContexts, fvars, mappedDSTIds, mappedFormulaIds);
						}

					}, this);

                }
            }
        }

        return {
            dsts: dstContexts,
            vars: fvars
        };
	},

	// This method will collect variables for a component as well as variables from all its peers.
	// The reason is that when DST is applied, it applies to all components (including peers).
	// To evaluate these formulas consistently with the same DST, the variables needs to be consistent as well.
	// Whether the current formula reference a variable or not, if the variable is used anywhere in the dst, it need to be present.
	collectDstVariables: function(resolveValues) {
		return DX.Formula.collectDstVariables(this.cx, resolveValues);

	},


	getFormulaText: function() {
		var formulaText = this.formula;
		var klip;

		if (this.cx.usePostDSTReferences() || (this.cx.hasDstActions(true) && this.cx.isVCFeatureOn())) {
			formulaText = this.cx.getDstHelper().getVCReferenceText();
		} else {
			klip = this.cx.getKlip();
			formulaText = DX.Formula.replaceFormulaReferences(this.formula, klip, true);
		}

		formulaText = DX.Formula.trim(formulaText, true);

		return formulaText;
	},

	usesVariable: function(n) {
		var i, klip, usesVar, vars;
		if (this.variables && this.variables.length != 0) {
			for(i = 0 ; i < this.variables.length; i++)
				if ( this.variables[i] == n ) return true;
		}

		klip = this.cx.getKlip();
		if (klip.datasourceProps && klip.datasourceProps.length != 0) {
			for( i = 0 ; i < klip.datasourceProps.length; i++ )
				if ( klip.datasourceProps[i] == n ) return true;
		}

		usesVar = false;
		if (this.cx.hasDstActions(true) || this.cx.usePostDSTReferences()) {
			vars = this.collectDstVariables(false);
			Object.keys(vars).forEach(function (key) {
				if (key == n) {
					usesVar = true;
				}
			});
		}

		return usesVar;
	},

    referencesAreEqual: function(ref1, ref2) {
        return ref1.type == ref2.type && ref1.id == ref2.id;
    },

    referenceExists: function(ref) {
		var i;

		if (this.references) {
            for (i = 0; i < this.references.length; i++) {
                if (this.referencesAreEqual(ref, this.references[i])) return true;
            }
        }

        return false;
    },

	onKlipAssigned: function() {
		if (!this.references) {
			this.initializeFormulaReferences();
		}
		if (!this.resultsReferences) {
			this.initializeResultsReferences();
		}

		if (this._trySettingVariablesLater) {
			this.setVariables();
		}
	},

	convertToTextOnlyFormat: function() {
		var klip, expandedReferences;

		if (this.version !== DX.Formula.VERSIONS.TYPE_IN && this.src && this.formula) {
			if (!this.references) {
                this.initializeFormulaReferences();
			}
			if (!this.resultsReferences) {
				this.initializeResultsReferences();
				//No conversion needed as these did not exist with click-click formula bar.
			}
			if (this.references) {
				klip = this.cx.getKlip();

				expandedReferences = DX.Formula.getExpandedReferences(this.references, klip);

				// Sort expanded references in descending order based on length
				expandedReferences.sort(function(a, b) {
					return b.formulaText.length - a.formulaText.length;
				});

				expandedReferences.forEach(function(reference) {
					var newReferenceString = DX.Formula.getComponentReferenceString(reference.componentId);
					var wrappedFormulaText = "(" + reference.formulaText + ")";
					this.formula = this.formula.split(wrappedFormulaText).join(newReferenceString);
				}, this);
			}
		}

		this.version = DX.Formula.VERSIONS.TYPE_IN;
	}
});

DX.Formula.currentFormulaIdGen = 1;

DX.Formula.COMPONENT_REFERENCE_REGEX = /!'[\da-fA-F-]+'/g;
DX.Formula.RESULTS_REFERENCE_REGEX = /&'[\da-fA-F-]+'/g;
DX.Formula.DATASOURCE_ADDRESS_REGEX = /[\da-fA-F]{32}@/g;
DX.Formula.DOUBLE_QUOTE_STRING_REGEX = /(?:"(\\.|[^"\\])*")/g;

DX.Formula.FORMULA_REF = 0;
DX.Formula.RESULTS_REF = 1;

DX.Formula._getMatches = function(formulaText, regex) {
	var matchArray = [];
	var match, string, startIndex;

	match = regex.exec(formulaText);
	while (match) {
		string = match[0];
		startIndex = match.index;

		matchArray.push({
			string: string,
			startIndex: startIndex,
			stopIndex: string.length + startIndex
		});

		match = regex.exec(formulaText);
	}

	return matchArray;
};

DX.Formula.getReferenceMatches = function(formulaText, refType) {
	var isResultsReference = (refType === DX.Formula.RESULTS_REF);
	var regex = isResultsReference ? DX.Formula.RESULTS_REFERENCE_REGEX : DX.Formula.COMPONENT_REFERENCE_REGEX;

	return DX.Formula._getReferenceMatches(formulaText, regex);
};

//References matches include all formulas that contain the following patterns: 
// - New Model & New Column: temporarydataset#newcolumn${column_id}
// - Existing Model & New Column: ${dataset_id}{32}#newcolumn${column_id}
// - Existing Model & Existing Column: ${dataset_id}{32}#${column_id}{32}
DX.Formula.getReferenceMatchesModeller = function(formulaText) {
	var DATASET_REFERENCE_REGEX = /([\da-fA-F]{32}|temporarydataset)#([\da-fA-F]{32}|newcolumn[\w\d]*);/g;
	return DX.Formula._getReferenceMatches(formulaText, DATASET_REFERENCE_REGEX);
};

DX.Formula._getReferenceMatches = function(formulaText, regex) {
	var referenceMatches = [];
	var newMatches, i, match;
	var callback = function(currentFormulaChunk, processedOffset) {
		newMatches = DX.Formula._getMatches(currentFormulaChunk, regex);

		// Must account for offset of processed string
		for (i=0; i < newMatches.length; i++) {
			match = newMatches[i];
            match.startIndex += processedOffset;
            match.stopIndex += processedOffset;
		}

		referenceMatches = referenceMatches.concat(newMatches);

		return currentFormulaChunk;
	};

	DX.Formula.transform(formulaText, callback, false);

	return referenceMatches;
};

DX.Formula.getComponentId = function(componentReferenceString) {
	return componentReferenceString.substring(2, componentReferenceString.length - 1); //remove opening "!'" and closing "'"
};

DX.Formula.getComponentReferenceString = function(componentId) {
	return "!'" + componentId + "'";
};

DX.Formula.getResultsReferenceString = function(componentId) {
	return "&'" + componentId + "'";
};

DX.Formula.getResultsReferenceFormulaModeller = function(dataSetId, columnId) {
	return dataSetId + "#" + columnId + ";";
};

/**
 * Replace all the component references in the specified formulaText with the
 * virtual column indexes from the components being referred to.
 *
 * Note: This function will fail to replace all references if some of the components
 *       that are referred to by the formulaText have not yet been added to the klip.
 *
 * @param dstRoot DST root component
 * @param formulaText - The text containing the formula to replace
 * @param klip - The klip object which contains the components referred to by formulaText
 * @param immediateDependencies Object to add the immediate component dependents to
 * @returns A string containing the formula text with all the component references replaced with actual formulas or false if component ids being referred to aren't found in klip.
 */
DX.Formula.putVCReferences = function(dstRoot, formulaText, klip, immediateDependencies, allDependencies) {
	var componentReferenceMatches = DX.Formula.getReferenceMatches(formulaText, DX.Formula.RESULTS_REF);
	var referenceFreeFormulaText = formulaText;
	var indexModifier = 0;
	var allReferencesReplaced;

	allReferencesReplaced = componentReferenceMatches.every(function(componentReferenceMatch) {
		var vcRef, refDstRoot;
		var matchLength = componentReferenceMatch.string.length;
		var componentId = DX.Formula.getComponentId(componentReferenceMatch.string);
		var component = klip.getComponentById(componentId);
		var referenceFormulaLength, preText, postText;

		if (component) {
			refDstRoot = component.getDstRootComponent();
			if (immediateDependencies && dstRoot == refDstRoot) {
				immediateDependencies[component.getVirtualColumnId()] = true;
			}
			if (allDependencies) {
				allDependencies[component.getVirtualColumnId()] = true;
			}
			vcRef = component.getDstHelper().getVCReferenceText();

			referenceFormulaLength = vcRef.length;
			preText = referenceFreeFormulaText.substr(0, componentReferenceMatch.startIndex + indexModifier);
			postText = referenceFreeFormulaText.substr(componentReferenceMatch.stopIndex + indexModifier);

			referenceFreeFormulaText = preText + vcRef + postText;

			indexModifier += referenceFormulaLength - matchLength; //replacements change the length of the string

			return true;
		}

		return false;
	}, this);

	return allReferencesReplaced && referenceFreeFormulaText;
};

/**
 * Replace all the component references in the specified formulaText with the
 * actual formulas from the components being referred to.
 *
 * Note: This function will fail to replace all references if some of the components
 *       that are referred to by the formulaText have not yet been added to the klip.
 *
 * @param formulaText - The text containing the formula to replace
 * @param klip - The klip object which contains the components referred to by formulaText
 * @param wrappingParenthesis - Used to enforce operator precedence when preparing formula for submission
 * @param visited
 * @returns A string containing the formula text with all the component references replaced with actual formulas or false if component ids being referred to aren't found in klip.
 */
DX.Formula.replaceFormulaReferences = function(formulaText, klip, wrappingParenthesis, visited) {
	var componentReferenceMatches = DX.Formula.getReferenceMatches(formulaText, DX.Formula.FORMULA_REF);
	var referenceFreeFormulaText = formulaText;
	var indexModifier = 0;
	var trimmedFormula, allReferencesReplaced;
	if (!visited) {
		visited = {};
	}

	allReferencesReplaced = componentReferenceMatches.every(function(componentReferenceMatch) {
		var matchLength = componentReferenceMatch.string.length;
		var componentId = DX.Formula.getComponentId(componentReferenceMatch.string);
		var component = klip.getComponentById(componentId);
		var referenceFormula, referenceFormulaText, recursiveFormulaText, referenceFormulaLength, preText, postText;

		if (component) {
			referenceFormula = component.getFormula && component.getFormula(0);

			if (referenceFormula && !visited[componentId]) {
				visited[componentId] = { recursiveFormulaText: "" };

				trimmedFormula = DX.Formula.trimComments(referenceFormula.formula, wrappingParenthesis);
				recursiveFormulaText = DX.Formula.replaceFormulaReferences(trimmedFormula, klip, wrappingParenthesis, visited);

				visited[componentId] = { recursiveFormulaText: recursiveFormulaText };
			} else {
				if (visited[componentId]) {
					recursiveFormulaText = visited[componentId].recursiveFormulaText;
				} else {
					// use empty string if component doesn't have a formula (e.g. formula was deleted after reference was created)
					// or if there's a circular reference.
					recursiveFormulaText = "";
					visited[componentId] = { recursiveFormulaText: recursiveFormulaText };
				}

			}

			if (false !== recursiveFormulaText) {
                referenceFormulaText = recursiveFormulaText;

                if(wrappingParenthesis) {
                    referenceFormulaText = "(" + referenceFormulaText + ")";
                }

				referenceFormulaLength = referenceFormulaText.length;
				preText = referenceFreeFormulaText.substr(0, componentReferenceMatch.startIndex + indexModifier);
				postText = referenceFreeFormulaText.substr(componentReferenceMatch.stopIndex + indexModifier);

				referenceFreeFormulaText = preText + referenceFormulaText + postText;

				indexModifier += referenceFormulaLength - matchLength; //replacements change the length of the string

				return true;
			}
		}

		return false;
	}, this);

	return allReferencesReplaced && referenceFreeFormulaText;
};

DX.Formula.putVCReferencesAndReplaceFormulaReferences = function(formulaText, component) {
    var finalFormulaText = DX.Formula.replaceFormulaReferences(formulaText, component.getKlip(), true);
    finalFormulaText = DX.Formula.putVCReferences(component.getDstRootComponent(), finalFormulaText, component.getKlip(), component._dependenciesFromSameRoot, component._allDependencies);
    return finalFormulaText;
};

DX.Formula.getExpandedReferences = function(references, klip) {
	var expandedReferences = [];

	references.forEach(function(reference) {
		var referenceComponentId = reference.id;
		var referenceComponent, referenceFreeFormulaText;
		var referenceFormula;
		var isIndirectReferenceToBlank;


		if(referenceComponentId) {
			referenceComponent = klip.getComponentById(referenceComponentId);

			if(referenceComponent) {
				referenceFormula = referenceComponent.getFormula(0);
				isIndirectReferenceToBlank = referenceFormula && referenceFormula.formula == "()";
				if (referenceFormula && !isIndirectReferenceToBlank) {
					// get formula text in the same format as it would be sent
					// to the DPN, including wrapping parentheses
					referenceFreeFormulaText = DX.Formula.replaceFormulaReferences(referenceFormula.formula, klip, true);
					if (referenceFreeFormulaText) {
						expandedReferences.push({
							componentId: referenceComponentId,
							formulaText: referenceFreeFormulaText
						});
					}

				}
			}
		}
	});

	return expandedReferences;
};

/*
 *	startString: string that starts the capturing expression
 *	regex: used to match the capturing expression to find where it ends so it can either be trimmed or ignored
 *	removable: can be safely removed without impacting formula evaluation
 */
DX.Formula.CAPTURING_EXPRESSIONS = [
	{
		startString: "\"",
		regex: /^(?:"(\\.|[^"\\])*")/,
		removable: false
	},
	{
		startString: "/*",
		regex: /^\/\*(?:.|\n|\r)+?\*\//,
		removable: true
	},
	{
		startString: "//",
		regex: /^\/\/.*\r?\n?/,
		removable: true
	},
	{
		startString: "@",
		regex: /^@[^;\n]*;/,
		removable: false
	}
];
DX.Formula.WHITESPACE_REGEX = /\s+/g;

/**
 * Trims all white space in a formula that isn't part of
 * a string, a line comment, a block comment, or a datasource reference.
 */
DX.Formula.trim = function(formulaText, trimRemovables) {
	var callback = function(currentFormulaChunk) {
		return currentFormulaChunk.replace(DX.Formula.WHITESPACE_REGEX, ""); //trim it!;
	};

	return DX.Formula.transform(formulaText, callback, trimRemovables);
};

DX.Formula.trimComments = function(formulaText) {
	var callback = function(currentFormulaChunk) {
		return currentFormulaChunk;
	};

	return DX.Formula.transform(formulaText, callback, true);
};

DX.Formula.transform = function(formulaText, transformCallback, trimRemovables) {
	var unprocessedFormula = formulaText;
	var transformedFormula = "";
	var indexOfFirstCapturingExpression, currentFormulaChunk, currentCapturingExpression, match, ignoredChunk;
	var i, element, startString, index;

	if (!unprocessedFormula) {
		return unprocessedFormula;
	}

	while (unprocessedFormula.length > 0) {
		indexOfFirstCapturingExpression = unprocessedFormula.length;
		currentCapturingExpression = null;
		ignoredChunk = "";

		// look for capturing startString with smallest index


        for (i=0; i < DX.Formula.CAPTURING_EXPRESSIONS.length; i++) {
            element = DX.Formula.CAPTURING_EXPRESSIONS[i];
            startString = element.startString;
            index = unprocessedFormula.indexOf(startString);
            if (-1 !== index && index < indexOfFirstCapturingExpression) {
                indexOfFirstCapturingExpression = index;
                currentCapturingExpression = element;
            }
        }

		currentFormulaChunk = unprocessedFormula.substring(0, indexOfFirstCapturingExpression);

		transformedFormula += transformCallback(currentFormulaChunk, transformedFormula.length);

		unprocessedFormula = unprocessedFormula.substring(indexOfFirstCapturingExpression);

		// find the end of the capturing expression and ignore it
		// if capturing expression isn't terminated, move past its startString
		if (currentCapturingExpression) {
			match = unprocessedFormula.match(currentCapturingExpression.regex);
			if (match && match[0]) {
				ignoredChunk = match && match[0];
			} else {
				ignoredChunk = currentCapturingExpression.startString;
			}

			if (!trimRemovables || !currentCapturingExpression.removable) {
				transformedFormula += ignoredChunk;
			}

			unprocessedFormula = unprocessedFormula.substring(ignoredChunk.length);
		}
	}

	return transformedFormula;
};

DX.Formula.determineRequiredDSTContexts = function (cx, dstCache, variableValueOverrides) {

    var dstContexts = [];
    var dstContext;
    var rootDSTComponent;
    var klip = cx.getKlip();
    var root = cx.getDstRootComponent();
    var fvars = {};
    var i, component;

    var others = klip.getOtherDstRootsNeeded(root.id);
    for (i=0; i < others.length; i++) {
        component = klip.getComponentById(others[i]);

        if (component) {
            $.extend(fvars, DX.Formula.collectDstVariables(component, true));
            $.extend(fvars,DX.Formula.addPropsToFVars(klip, fvars,  variableValueOverrides));
            $.extend(fvars,DX.Formula.addDynamicDatasourceProperties(klip, fvars,  variableValueOverrides));

			dstContext = component.getDstContextForEvaluation(fvars, dstCache);

			//Even if post-dst is off, we still add the current component's dst
			if (!dstContext.id) {
				//We are dealing with a klip created pre-post-dst. Add in the id from the dstRoot
				rootDSTComponent = component.getDstRootComponent();
				if (rootDSTComponent) {
					dstContext.id = rootDSTComponent.getDstHelper().getId();
				}
			}
			dstContexts.push(dstContext);
		}
	}


    return {
        dsts: dstContexts,
        vars: fvars
    };
};

DX.Formula.getPrimaryDSTFromFormula = function (formula) {
	var primaryDST = formula.dst;
	var dst;
	var i;

	if (formula.dst && formula.dst.length) {
        primaryDST = null;
        //It's an array, so we need to look for the dst whose id matches the prefix of the formula id
		for (i = 0; i < formula.dst.length; i++) {
			dst = formula.dst[i];
			if (dst.id && formula.fm.indexOf(dst.id) === 0) {
				primaryDST = dst;
				break;
			}
		}
	}

	return (primaryDST)? primaryDST : null;
};

DX.Formula.collectDstVariables = function (cx, resolveValues) {
    var fvars = {}, dstVars = {};
    var i, j, k;
    var dstPeer, formula, name, value;
    var klip = cx.getKlip();
    var dstPeers = cx.getDstPeerComponents();
    var dstRootComponent = cx.getDstRootComponent();
    if (dstPeers && dstPeers.length > 0) {
        for (i = 0; i < dstPeers.length; i++) {
            dstPeer = dstPeers[i];
            if (dstPeer.formulas && dstPeer.formulas.length > 0) {
                for (j = 0; j < dstPeer.formulas.length; j++) {
                    formula = dstPeer.formulas[j];
                    if (formula._trySettingVariablesLater) {
                        formula.setVariables();
                    }
                    if (formula.variables && formula.variables.length > 0) {
                        for (k = 0; k < formula.variables.length; k++) {
                            name = formula.variables[k];
                            if (resolveValues) {
                                value = klip.getKlipProp(name);
                                if (value !== undefined) {
                                    fvars[name] = value;
								}
                            } else {
                                fvars[name] = null;
                            }
                        }
                    }
                }
            }
        }
    }
    // Collect all the variables used in dst operations.(i.e filter).
    dstVars = dstRootComponent.collectDstFilterVariables();
    $.extend(fvars, dstVars);

    return fvars;
};

DX.Formula.addPropsToFVars = function(klip, fVars, propNames) {
    var len;
    var n;
    var v;

    if (klip && propNames && propNames.length > 0) {
        len = propNames.length;
        while(--len > -1) {
            n = propNames[len];
            v = klip.getKlipProp(n);
            if ( v !== undefined ) {
                fVars[n] = v;
            }
        }
    }
};

DX.Formula.addDynamicDatasourceProperties = function(klip, fVars, variableValueOverrides) {
    // send along properties required for dynamic datasources
    try {
        this.addPropsToFVars(klip, fVars, klip.datasourceProps );
    } catch(e) {

        console.log(e); // eslint-disable-line no-console

    }
    if(variableValueOverrides){
        $.each(fVars, function(key) {
            if (variableValueOverrides[key]) {
                fVars[key] = decodeURI(variableValueOverrides[key]);
            }
        });
    }

    return fVars;
};

DX.Formula.VERSIONS = {
	TYPE_IN: 2
};
DX.Formula._addFormulaIfNotAdded = function (formulasToRewatch, formulaMap, formula) {
  if (formula && !formulaMap[formula.id]) {
    formulasToRewatch.push(formula);
    formulaMap[formula.id] = true;
  }
};

DX.Formula.getFormulasToRewatch = function (formulas) {
  var formula, i, j, k, peers;
  var formulaMap = {};
  //copy the existing array.
  var formulasToRewatch = formulas.slice(0);
  var klip, root;
  var component;
  var others;

  for (i = 0; i < formulas.length; i++) {
    formula = formulas[i];
    formulaMap[formula.id] = true;
    if (formula.cx && formula.cx.usePostDSTReferences()) {
      if (!klip) {
        klip = formula.cx.getKlip();
      }
			root = formula.cx.getDstRootComponent();

      if (klip) {
        //This is a full list of other DST roots which refer to this one (included multiple levels of dependencies)
        others = klip.getDstRootDependents(root.id);
        for (j = 0; j < others.length; j++) {
          component = klip.getComponentById(others[j]);
          if (component) {
            //Always need to send all peers
            peers = component.getDstPeerComponents();
            for (k=0; k < peers.length; k++ ) {
              DX.Formula._addFormulaIfNotAdded(formulasToRewatch, formulaMap, peers[k].getFormula());
            }
          }
        }
			}
    }
  }

  return formulasToRewatch;
};
;/****** dx.manager.js *******/ 

/* global DX:false, dashboard:false, safeObjectNavigation:false, printStackTrace:false, statusMessage:false, setScrollPosition:false, getScrollPosition:false, newRelicNoticeError:false, isWorkspace:false*/
/**
 * manages the retrieval of data from the server.
 * maintains a list of all datasources that the dashboard wants updates from
 *
 * concerns:
 *		 - too many managed datasources if DS parameters are used excessively
 *		 - need a way to clean them
 *
 */


DX.PubSub_RegiserDatasource = "register_datasource";
DX.cxDataListeners = [];


DX.Manager = $.klass({

    datasources : false,
    refreshTimer : false,
    idCounter : 1,
    defaultProxy : false,
    formulaBuffer : false,
    reconnect_delay : 750,
    doNotReconnect : false,
    isReconnecting : false,
    reconnectTimeout : false,
    connectAttempt : 0,
    currentDpn: false,
	initialConnect : false,
    assignParams : "",
    connectedSockJS: false,
    state : 1,
    dpnType : 1,
    stateHandler : false,
    connectionTimerInterval : false,
    connectionLightTimeout : false,
    companyFlushed: false,
    chunkedMessageId : 1,
    chunkSize : 0,
    disconnectTime: false,
    disconnectTimerInterval: false,
    formulaEvaluationTimes: false,
    formulaEvaluationTimerIntervals: false,

    initialize : function(){
        var _this = this;

        this.datasources = {};
        this.formulas = {};
        this.formulaBuffer = [];

        this.queryCallbackMap = {};
        this.queryKlipIdMap = {};
        this.queryIdCounter = 0;
        this.componentIdQueryIdMap = {};

        this.updatedComponents = {};
        this.unwatchBuffer = [];
        this.watchBuffer = [];
        this.queryBuffer = [];

        this.klipFmCounts = {};

        this.formulaEvaluationTimes = {};
        this.formulaEvaluationTimerIntervals = {};

        PubSub.subscribe(DX.PubSub_RegiserDatasource, $.bind(this.onRegisterDatasource, this));
        this.updateLocalComponentData = _.throttle(_.bind(this.updateLocalComponentData, this), 1500);
        this.notifyDpnUnwatchFormulasDebounced = _.debounce(_.bind(this.notifyDpnUnwatchFormulasDebounced,this),50);
        this.notifyDpnWatchedFormulasDebounced = _.debounce(_.bind(this.notifyDpnWatchedFormulasDebounced,this),50);

        PubSub.subscribe("dashboard_busy", function(evtName, args) {
            if (args.busy) {
                _this.startFormulaEvaluationTimer(args.dashboardId);
            } else {
                _this.endFormulaEvaluationTimer(args.dashboardId);
            }
        });

        this.setConnectionStatus(false, "Not connected.");
    },


    connect : function(){
        var self;
        var assignParams;
        this.doNotReconnect = false;

        if ( !this.socket && typeof SockJS != "undefined" ){
            self = this;
            assignParams = {
                useDpn : safeObjectNavigation(KF, "useDpn", false),
                cid: KF.company.get("publicId")
            };

            self = this;

            return $.post("/DPN/ajax_AssignDpn", assignParams, $.bind(function(data){
                if(data.online){
                    if ( data.isCluster ) self.dpnType = DX.Manager.DpnType.CLUSTER;
                    console.log(getDateString() +  "connecting to type", self.dpnType  ); // eslint-disable-line no-console
                    self.stateHandler = _.bind( (data.isCluster ? clusterTransitionHandler : standardTransitionHandler ), self );
                    this.currentDpn = data;
                    this.connectRaw(data.protocol, data.host, data.port, assignParams.transport);
                }else{
                    this.currentDpn = false;
                }
            }, this));
        }
    },

    disconnect : function(allowReconnect){
        this.doNotReconnect = !allowReconnect;
        this.closeSocket();
    },

    closeSocket: function() {
        if (this.socket) {
            try {
                this.socket.close();
            } catch (err) {
                // do nothing; probably already closed the connection
            }

            this.socket = null;
            this.connectedSockJS = false;
        }
    },

    /**
     * asks the manager to get updates for the given datasource and address
     * @param dsid
     * @param address
     */
//	requestUpdates : function(dsid, address, callback)
//	{
//		var ds = this.datasources[dsid];
//		if (!ds)
//		{
//			ds = new DX.ManagedDatasource(dsid);
//			this.datasources[dsid] = ds
//		}
//
//		if (callback)
//		{
//			PubSub.subscribe(dsid + "_data", callback);
//		}
//
//		ds.addAddress(address);
//	},


    /**
     * pubsub message handler
     * @param msg will always be 'register_datasource'
     * @param args
     *	  id : the datasource ID
     *	  address: format-specific address to data range to retrieve
     *   parameters : object with key-values
     *   refreshInterval: time between data polls
     */
    onRegisterDatasource : function(msg, args){
        var ds = this.registerDatasource(args.id, args.parameters);
        ds.addAddress(args.address);
    },

    /**
     * when a datasource is registered we query the server for the datasource's identity --
     * a uniqueId based on the remoteId & parameters of the datasource.
     * @param remoteId the remoteId of the datasource
     * @param dsparams key-value query params
     * @param identity
     */
    registerDatasource : function(remoteId, dsparams, identity){
        // get existing managed datasource, otherwise create a new one
        var ds = new DX.ManagedDatasource(remoteId, dsparams);
        var i;

        for (i in this.datasources){
            if (!this.datasources.hasOwnProperty(i)) continue;
            if (this.datasources[i].equals(ds)) return this.datasources[i];
        }

        if(!identity){
            $.ajax({
                type:"POST",
                async:false,
                url:"/datasources/identify",
                data: { remoteId: remoteId, dsparams: dsparams },
                success: function(data){
                }
            });

            ds.id = this.idCounter++; // assign unique id to ds for this client
        }else{
            ds.id = identity;
        }

        ds.proxy = this.defaultProxy;

        this.datasources[ds.id] = ds;
        return ds;
    },


    /**
     *
     * @param f
     */
    registerFormula : function(f){
        this.formulas[f.id] = f;
    },

    /**
     * Remove a component from the formula list
     * @param f
     */
    deregisterFormula : function(formulaIndex){
        if(!isNaN(formulaIndex) ) {
            delete this.formulas[formulaIndex];
        }
   },

    unwatchKlip: function(klip) {
        this.unwatchFormulas(klip.getFormulas());
        delete this.klipFmCounts[klip.id];
    },

    /**
     * tell the dpn to stop watching a specific formula.
     * the fid will be queued in a buffer to be processed after 50ms the last call to the this method
     * @param formulas
     */
    unwatchFormulas : function( formulas ){
        var i;
        var formula;
        if (_.isArray(formulas) ){
            for(i = 0 ; i < formulas.length ; i++ ){
                formula = formulas[i];

                this.unwatchBuffer.push(formula.id);

                // remove data from local storage
                $.jStorage.deleteKey(formula.cx.id);
            }
        }else{
            this.unwatchBuffer.push(formulas.id);

            // remove data from local storage
            $.jStorage.deleteKey(formulas.cx.id);
        }

        this.notifyDpnUnwatchFormulasDebounced();

    },

    /**
     * this method is debounced in the constructor.
     */
    notifyDpnUnwatchFormulasDebounced : function(){
        if ( this.unwatchBuffer.length == 0 ) return;
        this.send( {cmd:"unwatch", ids: this.unwatchBuffer } );
        this.unwatchBuffer = [];
    },

    shouldCache : function() {
      return this.cacheDst;
    },

    /**
     * klipFms = [
     *      {
     *          kid: ""
     *          fms: []
     *      },
     *      ...
     * ]
     *
     * @param klipFms
     * @param noEval
     */
    watchFormulas : function( klipFms, noEval, variableValueOverrides ) {
        var busyKlips = [];
        var i,j;
        var kid;
        var fms;
        var fAsMsg;

        var requiredContexts;
        var dstCache = {};
        var usePostDSTReferences = false;

        for (i = 0; i < klipFms.length; i++) {
            kid = klipFms[i].kid;
            fms = klipFms[i].fms;
            requiredContexts = null;

            if (fms.length > 0) {
                usePostDSTReferences = true;
                //saas-8905 If any formula has no cx (which can happen if they are already serialized), don't attempt post-DST
                for (j = 0; j < fms.length; j++) {
                    if (usePostDSTReferences && (!fms[j].cx || !fms[j].cx.usePostDSTReferences())) {
                        usePostDSTReferences = false;
                    }
                }
            }

            if (!noEval && fms.length > 0) {
                // increase formula counter
                if (!(kid in this.klipFmCounts)) this.klipFmCounts[kid] = 0;
                this.klipFmCounts[kid] += fms.length;
                busyKlips.push(kid);
            }

            for (j = 0; j < fms.length; j++) {
                if (fms[j].serialize) {
                    fAsMsg = fms[j].serialize(variableValueOverrides, dstCache);

                    if (usePostDSTReferences) {

                        if (!dstCache[kid]) {
                            //Make sure we keep a separate cache per klip, so we don't run into collisions with dsts that have the same ids
                            dstCache[kid] = {};
                        }
                        requiredContexts = DX.Formula.determineRequiredDSTContexts(fms[j].cx, dstCache[kid], variableValueOverrides);

                        //clone the dsts array
                        fAsMsg.dst = requiredContexts.dsts.slice(0);
                        fAsMsg.vars = requiredContexts.vars;
                    }
                } else {
                    //Already serialized
                    fAsMsg = fms[j];
                }

                if (isWorkspace()) {
                    //Limit the size of results in the editor
                    fAsMsg.maxResultsSize = DX.Manager.MAX_EDITOR_QUERY_RESULTS_SIZE;
                }

                if ( noEval ) fAsMsg.no_eval = noEval;

                this.watchBuffer.push(fAsMsg);
            }
        }

        // notify that some klips are busy
        if (busyKlips.length > 0) {
            PubSub.publish("klip_states_changed", {
                state: { busy: true },
                klips: busyKlips
            });
        }

        this.notifyDpnWatchedFormulasDebounced();
    },

    notifyDpnWatchedFormulasDebounced : function(){
        var uniqueIds = {};
        var freeKlips = [];
        var i, watchList, fToWatch,
            watch, sameFormula;

        // only send watch commands when provisioned
        // -----------------------------------------
        if ( this.dpnType == DX.Manager.DpnType.CLUSTER && this.state != DX.Manager.State.PROVISIONED  ){
            return;
        }

        // do try to send watch info if we are not connected.  otherwise they will
        // watch states will be lost like tears in the rain.
        if (!this.initialConnect){
            setTimeout( this.notifyDpnWatchedFormulasDebounced , 200 );
            return;
        }


        if ( this.watchBuffer.length == 0 ) return;

        // send watch list, 10 items at a time to prevent overflow issues
        // --------------------------------------------------------------


        // reverse the list to process newer requests first..this allows us to prune
        // older duplicates which will now be at the end of the list.  we'll store
        // the IDs of formulas as we process them
        // -------------------------------------------------------------------------
        this.watchBuffer.reverse();

        while ( this.watchBuffer.length > 0 ){
            watchList = [];

            for (i = 0; i < 10; i++){
                fToWatch = this.watchBuffer.shift();

                if ( !uniqueIds[fToWatch.id] ) {  // if we haven't seen this ID yet, watch it
                    watch = true;
                    // if the first instance of the formula is no_eval, skip over it so an identical one
                    // that needs data can go out instead
                    if (fToWatch.no_eval) {
                        sameFormula = _.find(this.watchBuffer, function(f) {
                            return f.id == fToWatch.id;
                        });
                        if (sameFormula) {
                            watch = false;
                        }
                    }

                    if (watch) {
                        watchList.push( fToWatch );
                        uniqueIds[fToWatch.id] = 1;
                    }
                } else {
                    // do not decrement the counter for no_eval formulas because
                    // it was never incremented in the first place
                    if (!fToWatch.no_eval && fToWatch.kid in this.klipFmCounts) {
                        this.klipFmCounts[fToWatch.kid]--;

                        if (this.klipFmCounts[fToWatch.kid] == 0) {
                            delete this.klipFmCounts[fToWatch.kid];
                            freeKlips.push(fToWatch.kid);
                        }
                    }
                }

                if ( this.watchBuffer.length == 0 ) break;

            }

            if (watchList.length > 0) {
                // reverse the list again so that they are in the order originally watched
                watchList.reverse();
                this.send(this.createWatchRequest(watchList));
            }
        }

        if (freeKlips.length > 0) {
            PubSub.publish("klip_states_changed",  {
                state: { busy: false },
                klips: freeKlips
            });
        }
    },

    createWatchRequest: function(watchList) {
        var i,j;
        var dstMap = {};
        var dsts = [];
        var dst;
        var mapKey;
        var fm;
        var canSendSeparateDSTS = true;

        //This optimization can be applied in the general case (post-DST or not), however
        //to ensure that the DPN is new enough to handle it, only apply it if the post-DST
        //feature has been turned on
        if (KF.company.isPostDSTFeatureOn()) {

            //Any klips with dsts that have id's we can separate the dsts from the formulas and reduce the size of the request
            for (i = 0; i < watchList.length; i++) {
                fm = watchList[i];
                if (fm.dst && fm.dst.length) { //Has an array of dsts
                    canSendSeparateDSTS = true;

                    //First make sure all dsts associated to this formula have ids
                    for (j = 0; j < fm.dst.length; j++) {
                        dst = fm.dst[j];
                        if (!dst.id) {
                            canSendSeparateDSTS = false;
                        }
                    }
                    if (canSendSeparateDSTS) {
                        //Now collect all dsts in the formula, and add them to the map
                        //While replacing the dsts in the formula with their lookup ids
                        for (j = 0; j < fm.dst.length; j++) {
                            dst = fm.dst[j];
                            mapKey = dst.id + ":" + fm.kid;
                            if (!dstMap[mapKey]) {
                                dstMap[mapKey] = dst;
                            }
                            fm.dst[j] = dst.id;
                        }
                    }
                }
            }

            //Convert the dstMap to an array
            Object.keys(dstMap).forEach(function(key) {
                dsts.push(dstMap[key]);
            });

            return { cmd:"watch", f:watchList, dsts: dsts };
        }

        return { cmd:"watch", f:watchList };
    },

    /**
     * request formula execution on a query and register an optional callback on its completion.
     *
     * @param formula can be a string that contains the formula text, or an object that contains
     *    {
	 *      kid: <klip id>,
	 *      fm: <formula text> ,
	 *      cache: <tell dpn to cache result>,
	 *      vars: <object that contains vars and their values>
	 *    }
     * @param callback executed when result is available.
     * @param componentId can be the id of the component or undefined in case query is not
     *        associated with the component.
     */
    query: function (formula, callback, componentId) {
        var msg;
        var kid;

        if (_.isString(formula)) formula = { fm: formula };
        formula.id = "direct-query";
        formula.cache = false;

        msg = {
            cmd: "q",
            f: formula
        };

        msg = this.sendQueryMessage(msg, callback, componentId, formula.kid);

        kid = formula.kid;
        if (kid) {
            // increase formula counter
            if (!(kid in this.klipFmCounts)) this.klipFmCounts[kid] = 0;
            this.klipFmCounts[kid] += 1;
            this.queryKlipIdMap[msg.id] = kid;

            // notify that klip is busy
            PubSub.publish("klip_states_changed", {
                state: { busy: true },
                klips: [ kid ]
            });
        }
    },

    /**
     *
     * @param message - {
     *      type: "",
     *      dataSetId: "",
     *      columnId: "",
     * }
     * @param callback
     */
    modelSupportQuery: function(message, callback) {
        message.cmd = "model-support";

        this.sendQueryMessage(message, callback);
    },

    // supports multiples formulas (only for supporting data - not for components yet)
    sendMultipleWatchRequests: function (msg, callbacks) {
        var queryId;
        var formulas;
        var i;
        var _this = this;



        if (!this.initialConnect ||
          ( this.dpnType == DX.Manager.DpnType.CLUSTER && this.state != DX.Manager.State.PROVISIONED  )){
            setTimeout(function() {
                _this.sendMultipleWatchRequests(msg, callbacks);
            }, 200);
            return;
        }

        if(msg.f && callbacks && msg.f.length> 0 && msg.f.length === callbacks.length ) {
            formulas = msg.f;
            for(i = 0; i < formulas.length; i++) {
                // pre-pend data- to make the ID unique and avoid overlap with the klip formula ids
                queryId = "data-" + ++this.queryIdCounter;
                msg.f[i].id = queryId;
                this.registerFormula(msg.f[i]);

                this.queryCallbackMap[queryId] = callbacks[i];
            }


            // if it isn't safe to send queries we'll put them in the buffer
            // which will be processed on connect/provision
            // ------------------------------------------------------------
            if (this.queryServiceAvailable()) {
                this.send(msg);
            } else {
                this.queryBuffer.push(msg);
            }
            return msg;
        } else {
            return "";
        }
    },

    sendQueryMessage: function (msg, callback, componentId, kid) {

        var queryId = ++this.queryIdCounter;
        msg.id = queryId;

        // For each component, we will keep track of all the queries that get sent to dpn in componentIdQueryIdMap map i.e ({ componentId_kid : queryId } )
        // and override the `qid` if there are multiple queries. So componentIdQueryIdMap will always have the latest `qid` for a particular component.
      if (componentId && kid) {
          componentId = componentId + "_" + kid;
      } else {
          componentId = "query-" + queryId;
      }

        this.componentIdQueryIdMap[componentId] = queryId;

        this.queryCallbackMap[msg.id] = callback;

        // if it isn't safe to send queries we'll put them in the buffer
        // which will be processed on connect/provision
        // ------------------------------------------------------------
        if (this.queryServiceAvailable()) {
            this.send(msg);
        } else {
            this.queryBuffer.push(msg);
        }

        return msg;
    },

    processNonComponentQueryResults: function (msg, index) {
        var cb = this.queryCallbackMap[index];
        if (cb) {
            cb(msg.formulas[index], msg.errors[index]);
            this.deregisterFormula(index);
            delete this.queryCallbackMap[index];
        }
    },

    processQueryResults: function (msg) {
        var cb = this.queryCallbackMap[msg.qid];
        if (cb) {
            if (this.isLatestQueryResult(msg.qid, true)) {
                cb(msg.v, msg);
            }
            delete this.queryCallbackMap[msg.qid];
        }
    },

    /**
     *
     * @param queryId
     * @param cleanMap setting it to 'true' will delete matching key-value pair from the `componentIdQueryIdMap`.
     * @returns {boolean} : true if `queryId` from the result in found in `componentIdQueryIdMap` (map of component_id and latest query_id sent to dpn).
     */
    isLatestQueryResult: function (queryId, cleanMap) {
        var isLatest = false;
        var componentIdQueryIdMap = this.componentIdQueryIdMap;
        var componentId;

        for (componentId in componentIdQueryIdMap) {
            if (componentIdQueryIdMap.hasOwnProperty(componentId)) {
                if (queryId == componentIdQueryIdMap[componentId]) {
                    isLatest = true;
                    if (cleanMap) {
                        delete componentIdQueryIdMap[componentId];
                    }
                    break;
                }
            }
        }
        return isLatest;
    },

    /**
     * is it safe to send queries to the DPN?
     * @returns {boolean}
     */
    queryServiceAvailable : function(){
        switch (this.dpnType) {

            case DX.Manager.DpnType.STANDALONE:
                return this.state == DX.Manager.State.CONNECTED;

            case DX.Manager.DpnType.CLUSTER:
                return this.state == DX.Manager.State.PROVISIONED;
        }

    },

    /**
     * @returns {String} The name of the event that will be published when it is safe to send queries to the DPN
     *
     * Note: see queryServiceAvailable()
     */
    getServiceAvailableEventName : function(){
        return "dpn-connected";
    },

    /**
     *
     */
    dpnInfo : function(){
        this.send({cmd:"info"});
    },


    /**
     * sends a message over socket.io.
     * serializes the message to a JSON string and ensures connectivity
     * @param msg
     */
    send : function(msg){
        var serializedMsg;
        var numChunks;
        var part;
        var i, j;
        if (this.socket && this.connectedSockJS) {
            if (this.dpnType == DX.Manager.DpnType.STANDALONE && msg.cmd == "published_dashboard") {
                // the old dpns do not support tracking concurrent viewers of published dashboards
                // let the user view the dashboard
                this.handlePublishedDashboardMessage({ dashboardId:msg.dashboardId });
                return;
            }

            serializedMsg = JSON.stringify(msg);

            if ( this.chunkSize != 0 && serializedMsg.length > this.chunkSize) {
                this.chunkedMessageId++;
                numChunks = Math.ceil(serializedMsg.length / this.chunkSize);
                for (i = 0, j = 0; i < numChunks; ++i, j += this.chunkSize) {
                    part = serializedMsg.substr(j, this.chunkSize);
                    try {
                        this.socket.send("::chunk " + this.chunkedMessageId + " " + i + " " + numChunks + "|" + part);
                    } catch (err) {
                        newRelicNoticeError("SockJs Send Chunk Error: " + JSON.stringify($.extend(this.getContext(), { error: err })));
                    }
                }
            }else{
                try {
                    this.socket.send(serializedMsg);
                } catch (err) {
                    // observed always connectedSockJs=true, socket.readyState=0 when this error happens
                    PubSub.publish("sockjs-send-error", $.extend(this.getContext(), {
                        state: this.stateToString(),
                        message: serializedMsg
                    }));
                }
            }



        } else {
            if (msg.cmd == "q" || msg.cmd == "published_dashboard") {
                this.queryBuffer.push(msg);
            }
        }
    },

    /**
     * updates the local cache for the given ds & address.  if the update is 'new' data
     * an event is published with the new data.
     * @param dsid
     * @param address
     * @param val
     * @param useSync
     */
    setData : function(dsid, address, val, useSync){
        var ds = this.datasources[dsid];
        var evt;
        if (ds) {
            if (ds.setData(address, val)){
                evt = { dsid: dsid, address:address, data:val };
                if (useSync) PubSub.publishSync(dsid + "_data", evt);
                else PubSub.publish(dsid + "_data", evt);
            }
        }
    },


    onSocketConnect : function(){
        this.initialConnect = true;
        this.connectedSockJS = true;
        this.unexpectedDisconnect = false;
        this.stateHandler( DX.Manager.State.CONNECTED );
    },


    rewatchActiveTab : function(){
        var activeTab = dashboard.activeTab;
        var noEval = true;
        var klip;
        var watch;
        var len;

        // always want to rewatch formulas for imaging, otherwise transitioning from disconnected->connected will not re-eval formulas
        if (dashboard.config.imagingMode) {
            noEval = false;
        }

        if (activeTab) {
            dashboard.tabHistory = [ activeTab ];
            len = activeTab.klips.length;
            watch = [];
            while(--len > -1) {
                klip = activeTab.klips[len];
                watch.push({ kid: klip.id, fms: klip.getFormulas() });
            }

            if (watch.length > 0) {
                this.watchFormulas( watch, noEval );
            }
        }else{
            dashboard.tabHistory = [];
        }
    },


    onSocketMessage : function(msg){
        msg = JSON.parse(msg.data);

        switch (msg.type){
            // query result
            case "result":
                this.handleResultMessage(msg);
                break;

            // model support query result
            case "model-support-result":
                this.handleSupportResultMessage(msg);
                break;

            // formula update
            case "fup":
                this.handleFupMessage(msg);
                break;

            case "backend-provisioned":
                this.handleBackendProvisionedMessage(msg);
                break;

            case "backend-unprovisioned":
                this.handleBackendUnprovisionedMessage(msg);
                break;

            case "klip_update":
                this.handleKlipUpdateMessage(msg);
                break;

            case "tab_update":
                this.handleTabUpdateMessage(msg);
                break;

            case "klip_added":
                this.handleKlipAddedMessage(msg);
                break;

            case "klip_instance_removed":
                this.handleKlipInstanceRemovedMessage(msg);
                break;

            case "info":
                this.handleInfoMessage(msg);
                break;

            case "dpn_down":
                this.handleDpnDownMessage(msg);
                break;

            case "published-dashboard":
                this.handlePublishedDashboardMessage(msg);
                break;

            case "company-invalid":
                this.logoutUser();
                break;
        }
    },

    handleResultMessage : function(msg){
        var kid = this.queryKlipIdMap[msg.qid];

        if (kid) {
            // decrease formula counter
            if (kid in this.klipFmCounts) {
                this.klipFmCounts[kid]--;

                if (this.klipFmCounts[kid] === 0) {
                    delete this.klipFmCounts[kid];

                    // notify that klip is not busy anymore
                    PubSub.publish("klip_states_changed",  {
                        state: { busy: false },
                        klips: [ kid ]
                    });
                }
            }
        }

        this.processQueryResults(msg);
    },

    handleSupportResultMessage: function(msg) {
        this.processQueryResults(msg);
    },

  handleFupMessage: function (msg) {
    var f;
    var result;
    var cx;
    var klip;
    var kid;
    var freeKlips = [];
    var updateMessage;
    var resultCount;
    var cxFormula;

    for (f in msg.formulas) {
      if (msg.formulas.hasOwnProperty(f)) {
        try {
          result = msg.formulas[f];
          cxFormula = this.formulas[f];
          if (cxFormula && cxFormula.cx) {
            cx = cxFormula.cx;
            klip = cx.getKlip();
            kid = klip.id;

            // decrease formula counter
            if (kid in this.klipFmCounts) {
              resultCount = this.extractResultCountFromMessage(msg, f);

              this.klipFmCounts[kid] -= resultCount;

              if (this.klipFmCounts[kid] <= 0) {
                delete this.klipFmCounts[kid];
                freeKlips.push(kid);
              }
            }

            if (klip && klip.isPostDST()) {
              if (isWorkspace()) {
                if (msg.errors && msg.errors[f]) {
                  PubSub.publish("formula-evaluation-error", {
                    component: cx,
                    message: msg.errors[f].reason
                  });
                } else {
                  PubSub.publish("formula-evaluation-success", {
                    component: cx,
                    type: "component"
                  });
                }
              }
            }

            if (!_.isArray(result)) continue;

            // Create a message object in the format that's similar to a single formula result which DataComponent.setData expects.
            if (msg.metadata && msg.metadata[f]) {
              updateMessage = {v: result.slice(0), metadata: msg.metadata[f]};
            } else {
              updateMessage = {v: result.slice(0), metadata: {}};
            }

            cx.onFormulaUpdate(f, updateMessage); // use slice to copy the data to prevent shared references
            // problem: CXs support more than one formula and data cache (although there are no uses for more than one).
            // for now we assume it is always the first data/formula that was updated..
            // ---------------------------------------------------------------------------------------------------------

            if ((dashboard && dashboard.config && !dashboard.config.cacheDisabled) && !klip.cacheDisabled && !cx.cacheDisabled) {
              this.updatedComponents[cx.id] = result;
            }
          } else {
            this.processNonComponentQueryResults(msg, f);
          }
        } catch (e) {
          console.log(getDateString() + printStackTrace({e: e}).join("\n")); // eslint-disable-line no-console
          console.log(getDateString() + e); // eslint-disable-line no-console
        }
      }
    }

    // notify that some klips are not busy anymore
    if (freeKlips.length > 0) {
      PubSub.publish("klip_states_changed", {
        state: {busy: false},
        klips: freeKlips
      });
    }

    this.updateLocalComponentData();
  },

    handleBackendProvisionedMessage : function(msg){
        if ( msg.chunkSize ){
            console.log("setting chunk size: " , msg.chunkSize); // eslint-disable-line no-console
            this.chunkSize = msg.chunkSize;
        }

        this.stateHandler(DX.Manager.State.PROVISIONED);

    },

    handleBackendUnprovisionedMessage : function(msg){
        var self = this;
        this.companyFlushed = msg.flush;

        console.log(getDateString() +  "Unprovisioned"); // eslint-disable-line no-console

        this.closeSocket();

        setTimeout(function() {
            self.stateHandler(DX.Manager.State.DISCONNECTED);
        }, 1000);
    },

    handleKlipUpdateMessage : function(msg){
        var updateList;
        if (!dashboard) return;

        updateList = dashboard.getKlipsByMaster(msg.kid);

        if (updateList.length > 0) {
            _.each(updateList, function(klip) {
                dashboard.updateKlip(klip, msg, true);
            });

            statusMessage(updateList[0].title + " updated.", {info:true});
        }
    },

    handleTabUpdateMessage : function(msg){
        var tabs;
        var scrollPositions;
        if (!dashboard) return;

        tabs = dashboard.getTabsByMaster(msg.tid);
        if (tabs.length === 0) return; // user doesn't have tab

        _.each(tabs, function(t) {
            t.layout = dashboard.layouts[msg.layoutType];
            t.layoutType = t.layout.id;
            t.state = JSON.parse(msg.state);

            if (dashboard.activeTab && t == dashboard.activeTab) {
                scrollPositions = getScrollPosition();
                dashboard.setLayout(t.layoutType, t.state, false, true);
                setScrollPosition(scrollPositions);

                statusMessage("Dashboard updated by another user.", {info:true});
            }
        });
    },

    handleKlipAddedMessage : function(msg){
        var tabs;
        var klipConfig;
        var fullConfig;
        if (!dashboard) return;

        tabs = dashboard.getTabsByMaster(msg.tid, true);
        if (tabs.length === 0) return; // user doesn't have tab..

        klipConfig = JSON.parse(msg.schema);

        $.post("/dashboard/ajax_getKlipRights", {kid:msg.kid}, function(d) {
            // ajax-op will return 403 if no rights exists, and this callback will not be executed...should be safe to add
            dashboard.rights[msg.kid] = d;

            fullConfig = klipConfig;

            _.each(tabs, function(t) {
                dashboard.addKlip(fullConfig, t.id, true, true);
            });
        }, "json");
    },

    handleKlipInstanceRemovedMessage : function(msg){
        var removeList;
        if (!dashboard) return;

        removeList = dashboard.getKlipsById(msg.kid);

        if (removeList.length > 0) {
            _.each(removeList, function(klip) {
                dashboard.removeKlip(klip, { preventSerialization: true });
            });

            statusMessage(removeList[0].title + " deleted by another user.", {info:true});
        }
    },

    handleInfoMessage : function(msg){
        console.log(getDateString() +  "info", msg); // eslint-disable-line no-console
    },

    handleDpnDownMessage : function(msg){
        console.log(getDateString() +  "DPN shutdown"); // eslint-disable-line no-console
    },

    handlePublishedDashboardMessage : function(msg){
        PubSub.publish("published-dashboard-connection", { profileId:msg.dashboardId, overlimit:msg.overlimit });
    },


    /**
     * disconnected->connected: client should ident
     * connected->provisioned: watch formulas
     * provisioned->connected: warn user, block sends
     * *->disconnected: attempt reconnect
     * @param nextState
     */
    transitionToState : function(nextState){
        this.state = nextState;
    },

    /**
     *
     */
    sendIdent : function(){
        this.send({
            cmd: "ident",
            uid: KF.user.get("publicId"),
            cid: KF.company.get("publicId"),
            useIF: KF.company.hasFeature("use_if")
        });
    },

    /**
     * throttled function (done in 'initialize') which updates the browser local storage with the component data
     *
     * once this.updatedComponents is processed it is cleared.
     */
    updateLocalComponentData : function(){
        /* always disable cache for now due to performance issues - See saas-5326 for details.

         if ( isWorkspace() || (dashboard && dashboard.config && dashboard.config.cacheDisabled) ) return;

         _.each(this.updatedComponents, function(data, cxId) {
         // set data from local storage
         $.jStorage.set(cxId, data);
         });

         this.updatedComponents = {};
         */
    },

    onSocketDisconnect : function(){
        if (this.socket) {
            // closing the socket explicitly nulls this.socket
            // this is a case where an unexpected disconnect occurred
            this.messageQueue = [];
            if (this.messageQueueInterval) {
                window.clearInterval(this.messageQueueInterval);
                this.messageQueueInterval = null;
            }
            if (this.currentMessageIntervalIndex < DX.Manager.SAFARI_MESSAGE_INTERVALS_MS.length - 1) {
                this.currentMessageIntervalIndex++;
                console.log("Increasing messageInterval to " + DX.Manager.SAFARI_MESSAGE_INTERVALS_MS[this.currentMessageIntervalIndex] + " ms."); // eslint-disable-line no-console
            }
            this.unexpectedDisconnect = true;
        }

        if (typeof this.stateHandler == "function"){
            this.stateHandler( DX.Manager.State.DISCONNECTED );
        }
    },

    connectRaw: function (protocol, host, port){
        var _socket;
        var transport = { transport : safeObjectNavigation(KF, "transport", false) };
        console.log(getDateString() +  protocol+"://"+host+":"+port+"/echo"); // eslint-disable-line no-console
        if (transport.transport){
            console.log(getDateString() +  "Using transport layer: " + transport.transport); // eslint-disable-line no-console
            this.socket = new SockJS(protocol+"://"+host+":"+port+"/echo", null, {protocols_whitelist:[transport.transport]});
        }else if ($.browser.msie && $.browser.version == 9){
            this.socket = new SockJS(protocol+"://"+host+":"+port+"/echo", null, {protocols_whitelist:["xdr-streaming", "xdr-polling"]});
        }else {
            this.socket = new SockJS(protocol+"://"+host+":"+port+"/echo");
        }
        _socket = this.socket;

        _socket.onopen = $.bind(this.onSocketConnect, this);
        _socket.onmessage = $.bind(this.onSocketMessage, this);
        _socket.onclose = $.bind(this.onSocketDisconnect, this);
    },

    /**
     * Toggle the connect light of the dashboard and set the text
     *
     * @param connected
     * @param text
     */
    setConnectionStatus : function(connected, text){
        var self;
        var dpnInfo;
        var $connectionStatus = $("#connection-status");
        clearTimeout(this.connectionLightTimeout);

        if ($connectionStatus.length === 0){
            self = this;
            this.connectionLightTimeout = setTimeout( function(){
                self.setConnectionStatus(connected,text)
            },100);
            return;
        }

        $connectionStatus.html(text);


        dpnInfo = "ID: " + this.currentDpn.id +
            "\n Online: " + this.currentDpn.online;


        $connectionStatus.attr("title", (connected ? text + ":\n " + dpnInfo : text));

        if (connected) {
            $connectionStatus.addClass("connected").removeClass("disconnected");
        } else {
            $connectionStatus.removeClass("connected").addClass("disconnected");
        }
    },

    transitionToDisconnected: function() {
        var currentDpnId = this.currentDpn.id;

        console.log(getDateString() +  "Disconnected.."); // eslint-disable-line no-console

        PubSub.publish("dpn-disconnected", this.getContext());
        this.startDisconnectTimer();

        this.setConnectionStatus(false, "Not connected.");

        // clear all remaining spinners unless this is an unexpected disconnect
        if (!this.unexpectedDisconnect && !this.doNotReconnect) {
            PubSub.publish("klip_states_changed",  {
                state: { busy: false },
                klips: Object.keys(this.klipFmCounts)
            });
        }
        this.klipFmCounts = {};

        PubSub.publish("socket_connect", false);
        this.isReconnecting = true;
        this.connectedSockJS = false;

        if (typeof dashboard !== "undefined" && dashboard.config.imagingMode) $.post("/error/ajax_logMessage", {msg:"Imaging: Socket Disconnected [" + currentDpnId + "]"});
    },

    startDisconnectTimer: function() {
        var _this = this;

        if (this.disconnectTime && this.disconnectTimerInterval) return;

        this.disconnectTime = new Date().getTime();

        this.disconnectTimerInterval = setInterval(function() {
            PubSub.publish("dpn-disconnected-time", $.extend(_this.getContext(), {
                state: _this.stateToString(),
                timeElapsed: DX.Manager.DISCONNECT_INTERVAL
            }));
        }, DX.Manager.DISCONNECT_INTERVAL);
    },

    endDisconnectTimer: function() {
        var now;
        var numberOfIntervals;
        var remainingTime;

        if (!this.disconnectTime && !this.disconnectTimerInterval) return;

        clearInterval(this.disconnectTimerInterval);
        this.disconnectTimerInterval = false;

        now = new Date().getTime();
        numberOfIntervals = (now - this.disconnectTime) / DX.Manager.DISCONNECT_INTERVAL;
        remainingTime = Math.floor((numberOfIntervals - Math.floor(numberOfIntervals)) * DX.Manager.DISCONNECT_INTERVAL);

        if (remainingTime > DX.Manager.DISCONNECT_THRESHOLD) {
            PubSub.publish("dpn-disconnected-time", $.extend(this.getContext(), {
                state: this.stateToString(),
                timeElapsed: remainingTime
            }));
        }

        this.disconnectTime = false;
    },

    startFormulaEvaluationTimer: function(dashboardId) {
        var _this = this;

        if (this.formulaEvaluationTimes[dashboardId] && this.formulaEvaluationTimerIntervals[dashboardId]) return;

        this.formulaEvaluationTimes[dashboardId] = new Date().getTime();

        this.formulaEvaluationTimerIntervals[dashboardId] = setInterval(function() {
            PubSub.publish("formula-evaluation-time", $.extend(_this.getContext(), {
                state: _this.stateToString(),
                timeElapsed: DX.Manager.FORMULA_EVALUATION_INTERVAL
            }));
        }, DX.Manager.FORMULA_EVALUATION_INTERVAL);
    },

    endFormulaEvaluationTimer: function(dashboardId) {
        var now;
        var totalTime;
        var numberOfIntervals;
        var remainingTime;
        var timedDashboard;
        var dashboardMasterId;

        if (!this.formulaEvaluationTimes[dashboardId] && !this.formulaEvaluationTimerIntervals[dashboardId]) return;

        clearInterval(this.formulaEvaluationTimerIntervals[dashboardId]);
        delete this.formulaEvaluationTimerIntervals[dashboardId];

        now = new Date().getTime();
        totalTime = now - this.formulaEvaluationTimes[dashboardId];
        numberOfIntervals = totalTime / DX.Manager.FORMULA_EVALUATION_INTERVAL;
        remainingTime = Math.floor((numberOfIntervals - Math.floor(numberOfIntervals)) * DX.Manager.FORMULA_EVALUATION_INTERVAL);

        if (remainingTime > DX.Manager.FORMULA_EVALUATION_THRESHOLD) {
            PubSub.publish("formula-evaluation-time", $.extend(this.getContext(), {
                state: this.stateToString(),
                dashboardId: dashboardId,
                timeElapsed: remainingTime
            }));
        }

        if (dashboard) {
            timedDashboard = dashboard.getTabById(dashboardId);
            if (timedDashboard) {
                dashboardMasterId = timedDashboard.master;
            }
        }

        PubSub.publish("dashboard-load-time", $.extend(this.getContext(), {
            state: this.stateToString(),
            dashboardId: dashboardMasterId,
            timeElapsed: totalTime
        }));

        delete this.formulaEvaluationTimes[dashboardId];
    },

    stateToString: function() {
        switch (this.state) {
            case DX.Manager.State.DISCONNECTED:
                return "Disconnected";
            case DX.Manager.State.CONNECTED:
                return (this.dpnType === DX.Manager.DpnType.CLUSTER ? "Waiting for Provisioning" : "Connected");
            case DX.Manager.State.PROVISIONED:
                return "Connected";
            default:
                return "Unknown";
        }
    },

    getContext: function() {
        return {
            companyId: KF.company.get("publicId"),
            userId: KF.user.get("publicId"),
            context: typeof dashboard !== "undefined" ? dashboard.getContextString() : "",
            dpn: this.currentDpn.id
        };
    },

    extractResultCountFromMessage: function(msg, formulaIndex) {
        var resultCount = 1;

        if (msg.metadata) {
            if (msg.metadata[formulaIndex]) {
                if (msg.metadata[formulaIndex].resultCount) {
                    resultCount = msg.metadata[formulaIndex].resultCount;
                }
            }
        }

        return resultCount;
    },

    logoutUser: function() {
        require(["js_root/utilities/utils.browser"], function(browserUtil) {
            browserUtil.navigateTo("/users/logout", false);
        });
    }
});


function standardTransitionHandler(nextState) {

    var currentState = this.state;
    var currentDpnId = this.currentDpn.id;

    if (nextState == currentState) {
        console.log(getDateString() +  "illegal state transition (", nextState, ")"); // eslint-disable-line no-console
        return;
    }

    console.log(getDateString() +  "connection state change:", currentState, nextState); // eslint-disable-line no-console

    if (nextState == DX.Manager.State.CONNECTED){
        console.log(getDateString() +  "Connected"); // eslint-disable-line no-console

        this.endDisconnectTimer();
        PubSub.publish("dpn-connected", this.getContext());

        this.sendIdent();

        this.connectAttempt = 0;

        // if isReconnecting is set to true it means that we need to collect all the formulas in
        // use in the dashboard and send them to the DPN to be watched
        // -------------------------------------------------------------------------------------
        if (this.isReconnecting) {
            this.isReconnecting = false;
            this.rewatchActiveTab();
        }

        if (this.queryBuffer.length > 0) {
            while (this.queryBuffer.length > 0) {
                this.send(this.queryBuffer.shift());
            }
            this.queryBuffer = [];
        }

        this.formulaBuffer = [];

        PubSub.publish("socket_connect", true);

        this.setConnectionStatus(true, "Connected");

        if (typeof dashboard !== "undefined" && dashboard.config.imagingMode) $.post("/error/ajax_logMessage", {msg: "Imaging: Socket Connect [" + currentDpnId + "]"});
    }


    if (nextState == DX.Manager.State.DISCONNECTED){
        this.transitionToDisconnected();
    }

    this.transitionToState(nextState);
}

function clusterTransitionHandler(nextState){
    var currentState = this.state;
    var provisionAttempts = 0;
    var sendIdentTimeout;
    var self = this;
    var currentDpnId = this.currentDpn.id;

    if (nextState == currentState) {

        if(nextState == DX.Manager.State.DISCONNECTED) {
            this.transitionToDisconnected();
        }

        console.log(getDateString() +  "no state change (", nextState, ")"); // eslint-disable-line no-console
        return;
    }

    console.log(getDateString() +  "connection state change:", currentState, nextState); // eslint-disable-line no-console


    clearTimeout( sendIdentTimeout );

    clearInterval( this.connectionTimerInterval );

    if (nextState == DX.Manager.State.CONNECTED){
        console.log(getDateString() +  "Connected (waiting for provisioning)"); // eslint-disable-line no-console

        this.startDisconnectTimer();

        // clear reconnecting flag
        if (this.isReconnecting){
            this.isReconnecting = false;
            if (dashboard.config.imagingMode) {
                this.rewatchActiveTab();
            }
        }

        this.sendIdent();

        this.connectAttempt = 0;

        // schedule an additional ident in the future in case there was an initial failiure
        // possibly caused by timing issues / front-end not being ready..
        // --------------------------------------------------------------------------------
        sendIdentRequest();

        PubSub.publish("socket_connect", true);

        this.setConnectionStatus(true, "Connected (waiting for provisioning)");

        if (typeof dashboard !== "undefined" && dashboard.config.imagingMode) $.post("/error/ajax_logMessage", {msg:"Imaging: Socket Connected (waiting for provisioning) [" + currentDpnId + "]"});
    }


    if (nextState == DX.Manager.State.DISCONNECTED) {
        if (currentState == DX.Manager.State.PROVISIONED && !this.companyFlushed) {
            clearTimeout(this.reconnectTimeout);
            console.log(getDateString() + "Scheduling a reconnect in " + DX.Manager.DISCONNECT_RECONNECT_DELAY / 1000 + "s"); // eslint-disable-line no-console
            this.reconnectTimeout = setTimeout(reconnectCheck, DX.Manager.DISCONNECT_RECONNECT_DELAY);
            this.companyFlush = false;
        }

        this.transitionToDisconnected();
    }


    if ( nextState == DX.Manager.State.PROVISIONED ){
        console.log(getDateString() +  "Provisioned"); // eslint-disable-line no-console

        this.endDisconnectTimer();
        PubSub.publish("dpn-connected", this.getContext());

        this.setConnectionStatus(true, "Connected");

        this.notifyDpnWatchedFormulasDebounced();

        if (this.queryBuffer.length > 0) {
            while (this.queryBuffer.length > 0) {
                this.send(this.queryBuffer.shift());
            }
            this.queryBuffer = [];
        }
        this.rewatchActiveTab();

        if (typeof dashboard !== "undefined" && dashboard.config.imagingMode) $.post("/error/ajax_logMessage", {msg:"Imaging: Socket Provisioned [" + currentDpnId + "]"});
    }

    this.transitionToState(nextState);


    function sendIdentRequest(){
        sendIdentTimeout = setTimeout(function() {
            if (self.state == DX.Manager.State.CONNECTED) {
                provisionAttempts++;
                if (provisionAttempts > 8) {
                    self.closeSocket();
                    return;
                }
                console.log(getDateString() +  "sending ident"); // eslint-disable-line no-console
                self.sendIdent();
                sendIdentRequest();
            }
        }, nextInterval(2000, provisionAttempts));
    }

    function nextInterval(baseTime,attempt){
        var nextInterval = baseTime;
        if ( attempt < 1 ){
            nextInterval = baseTime;
            nextInterval += Math.random()*10000;
        }else if ( attempt < 4 ) nextInterval = baseTime * attempt;
        else nextInterval = (baseTime * attempt * 1.5);

        return nextInterval;
    }
}

function reconnectCheck() {
    var manager = DX.Manager.instance;
    if (!manager) return;

    clearTimeout(manager.reconnectTimeout);

    if ((!manager.doNotReconnect && manager.isReconnecting) || !manager.currentDpn) {
        if (manager.connectAttempt < DX.Manager.MAX_RECONNECT_ATTEMPTS) {
            manager.connectAttempt++;
            console.log(getDateString() + "trying to reconnect", manager.connectAttempt); // eslint-disable-line no-console
        } else {
            location.reload();
        }

        manager.socket = null;
        manager.connect();
        manager.reconnectTimeout = setTimeout(reconnectCheck, DX.Manager.DISCONNECT_RECONNECT_DELAY);
    } else {
        // nothing to do...schedule another check in a short delay
        // -------------------------------------------------------
        manager.reconnectTimeout = setTimeout(reconnectCheck, DX.Manager.CONTINUOUS_RECONNECT_DELAY);
    }
}

function getDateString() {
    var dt = new Date($.now());
    return "[" + dt.getHours() + ":" + dt.getMinutes() + ":" + dt.getSeconds() + "]: ";
}


DX.Manager.State = {
    DISCONNECTED : 1,
    CONNECTED : 2,
    PROVISIONED : 3
};

DX.Manager.DpnType = {
    STANDALONE  : 1,
    CLUSTER : 2
};

DX.Manager.SAFARI_MESSAGE_INTERVALS_MS = [100, 400, 800, 1000];

DX.Manager.instance = new DX.Manager();
DX.Manager.debugCallback = function(r){
    console.log(getDateString() +  r); // eslint-disable-line no-console
};

DX.Manager.CONTINUOUS_RECONNECT_DELAY = 5000;
DX.Manager.DISCONNECT_RECONNECT_DELAY = (KF.company.get("dpnReconnectDelay") || 5) * 1000;
DX.Manager.RELOAD_PAGE_DELAY = 3 * 60 * 1000; /* 3 minutes */
DX.Manager.MAX_RECONNECT_ATTEMPTS = Math.ceil(DX.Manager.RELOAD_PAGE_DELAY / DX.Manager.DISCONNECT_RECONNECT_DELAY);
DX.Manager.DISCONNECT_INTERVAL = 5000;
DX.Manager.DISCONNECT_THRESHOLD = 500;
DX.Manager.FORMULA_EVALUATION_INTERVAL = 5000;
DX.Manager.FORMULA_EVALUATION_THRESHOLD = 300;
DX.Manager.MAX_EDITOR_QUERY_RESULTS_SIZE = 300;

// if socketio/sockjs is required, start connection process onready
if ( window.socketio_required ){
    $(function(){
        DX.Manager.instance.startDisconnectTimer();
        DX.Manager.instance.connect();
        DX.Manager.reconnectTimeout = setTimeout( reconnectCheck , DX.Manager.CONTINUOUS_RECONNECT_DELAY );
    });
}
;