// From Ext library
/*global Ext*/
// Defined in dashboard.html
/*global AUTOCOMPLETE_DELAY CALENDAR_ICON CLOCK_ICON CONTEXT_FIELD_WIDTH FINDER_QUERY_DELAY*/
/*global HELP_ICON NEW_DASHBOARD_REMOVE_GRAPHS REFRESH_ICON REMOVE_ICON RESIZE_ICON*/
/*global SHARE_ICON UI_CONFIG initialState initialError permissions queryString userName*/
/*global UP_ICON DOWN_ICON TRASH_ICON permissionsUnauthenticated schemes*/
// Defined in composer_widgets.js
/*global createFunctionsMenu createOptionsMenu updateCheckItems*/

// Global object names
var viewport;
var contextSelector;
var contextSelectorFields = [];
var selectedScheme = null;
var selectedRecord = null;
var metricSelector;
var metricSelectorMode;
var metricSelectorGrid;
var metricSelectorTextField;
var graphArea;
var graphStore;
var graphView;
var navBar;
var dashboardName;
var dashboardURL;
var refreshTask;
var spacer;
var justClosedGraph = false;
var NOT_EDITABLE = ['from', 'until', 'width', 'height', 'target', 'uniq', '_uniq'];
var editor = null;

var cookieProvider = new Ext.state.CookieProvider({
  path: document.body.dataset.baseUrl + 'dashboard'
});

var NAV_BAR_REGION = cookieProvider.get('navbar-region') || 'north';

var CONFIRM_REMOVE_ALL = cookieProvider.get('confirm-remove-all') != 'false';

var currentlySettingHash = false;

function changeHash(hash){
    currentlySettingHash = true;
    window.location.hash = hash;
}

if ('onhashchange' in window) // does the browser support the hashchange event?
  window.onhashchange = function () {
    if (currentlySettingHash){
      currentlySettingHash = false;
      return;
    }
    location.reload();
  }

/* Nav Bar configuration */
var navBarNorthConfig = {
  region: 'north',
  layout: 'hbox',
  layoutConfig: { align: 'stretch' },
  collapsible: true,
  collapseMode: 'mini',
  collapsed: false,
  split: true,
  title: 'untitled',
  height: 350,
  listeners: {
    expand: function() { focusCompleter(); } // defined below
  }
};

var navBarWestConfig = Ext.apply({}, navBarNorthConfig);
delete navBarWestConfig.height;
navBarWestConfig.region = 'west';
navBarWestConfig.layout = 'vbox';
navBarWestConfig.width = 338;


// Record types and stores
var SchemeRecord = Ext.data.Record.create([
  {name: 'name'},
  {name: 'pattern'},
  {name: 'fields', type: 'auto'}
]);

var schemeRecords = [];

var schemesStore = new Ext.data.Store({
  fields: SchemeRecord
});


var ContextFieldValueRecord = Ext.data.Record.create([
  {name: 'name'},
  {path: 'path'}
]);

var contextFieldStore = new Ext.data.JsonStore({
  url: document.body.dataset.baseUrl + 'metrics/find/',
  root: 'metrics',
  idProperty: 'name',
  fields: ContextFieldValueRecord,
  baseParams: {format: 'completer', wildcards: '1'}
});


var GraphRecord = new Ext.data.Record.create([
  {name: 'target'},
  {name: 'params', type: 'auto'},
  {name: 'url'},
  {name: 'width', type: 'auto'},
  {name: 'height', type: 'auto'},
  {name: 'loading'},
]);

var graphStore;
function graphStoreUpdated() {
  if (metricSelectorGrid) metricSelectorGrid.getView().refresh();
}

graphStore = new Ext.data.ArrayStore({
  fields: GraphRecord,
  listeners: {
    add: graphStoreUpdated,
    remove: graphStoreUpdated,
    update: graphStoreUpdated
  }
});

var originalDefaultGraphParams = {
  from: '-2hours',
  until: 'now',
  width: UI_CONFIG.default_graph_width,
  height: UI_CONFIG.default_graph_height
};
var defaultGraphParams;
//XXX
// Per-session default graph params
var sessionDefaultParamsJson = cookieProvider.get('defaultGraphParams');
if (sessionDefaultParamsJson && sessionDefaultParamsJson.length > 0) {
  defaultGraphParams = Ext.decode(sessionDefaultParamsJson);
} else {
  defaultGraphParams = Ext.apply({}, originalDefaultGraphParams);
}

function isLoggedIn() {
  return userName != null;
}

function hasPermission(permission) {
  for (const i in permissions) {
    if (permissions[i] === permission) {
      return true;
    }
  }
  return false;
}

function htmlEncode(input) {
  return input.replace(/[^a-zA-Z0-9 ]/g, function (chr) {
    return '&#' + chr.charCodeAt() + ';';
  });
}

function initDashboard () {

  // Populate naming-scheme based datastructures
  Ext.each(schemes, function (schemeInfo) {
    schemeInfo.id = schemeInfo.name;
    schemeRecords.push( new SchemeRecord(schemeInfo) );

    Ext.each(schemeInfo.fields, function (field) {

      // Context Field configuration
      contextSelectorFields.push( new Ext.form.ComboBox({
        id: schemeInfo.name + '-' + field.name,
        fieldLabel: field.label,
        width: CONTEXT_FIELD_WIDTH,
        mode: 'remote',
        triggerAction: 'all',
        editable: true,
        forceSelection: false,
        store: contextFieldStore,
        displayField: 'name',
        queryDelay: 100,
        queryParam: 'query',
        minChars: 1,
        typeAhead: false,
        value: queryString[field.name] || getContextFieldCookie(field.name) || '*',
        listeners: {
          beforequery: buildQuery,
          change: contextFieldChanged,
          select: function (thisField) { thisField.triggerBlur(); focusCompleter(); },
          afterrender: function (thisField) { thisField.hide(); },
          hide: function (thisField) { thisField.getEl().up('.x-form-item').setDisplayed(false); },
          show: function (thisField) { thisField.getEl().up('.x-form-item').setDisplayed(true); }
        }
      }) );

    });

  });
  schemesStore.add(schemeRecords);

  spacer = new Ext.form.TextField({
    hidden: true,
    hideMode: 'visibility'
  });

  var metricTypeCombo = new Ext.form.ComboBox({
    id: 'metric-type-field',
    fieldLabel: 'Metric Type',
    width: CONTEXT_FIELD_WIDTH,
    mode: 'local',
    triggerAction: 'all',
    editable: false,
    store: schemesStore,
    displayField: 'name',
    listeners: {
      afterrender: function (combo) {
        var value = (queryString.metricType) ? queryString.metricType : getContextFieldCookie('metric-type');

        if (!value) {
          value = 'Everything';
        }
        var index = combo.store.find('name', value);
        if (index > -1) {
          var record = combo.store.getAt(index);
          combo.setValue(value);
          metricTypeSelected.defer(250, this, [combo, record, index]);
        }
      },
      select: metricTypeSelected
    }
  });

  contextSelector = new Ext.form.FormPanel({
    flex: 1,
    autoScroll: true,
    labelAlign: 'right',
    items: [
      spacer,
      metricTypeCombo
    ].concat(contextSelectorFields)
  });

  function expandNode(node, recurse) {
    function addAll () {
      Ext.each(node.childNodes, function (child) {
        if (child.leaf) {
          graphAreaToggle(child.id, {dontRemove: true});
        } else if (recurse) {
          expandNode(child, recurse);
        }
      });
    }

    if (node.isExpanded()) {
      addAll();
    } else {
      node.expand(false, false, addAll);
    }
  }

  var folderContextMenu = new Ext.menu.Menu({
    items: [{
      text: 'Add All Metrics',
      handler: function (item, e) {
                 expandNode(item.parentMenu.node, false);
               }
    }, {
      text: 'Add All Metrics (recursively)',
      handler: function (item, e) {
                 expandNode(item.parentMenu.node, true);
               }
    }]
  });

  if (NAV_BAR_REGION == 'west') {
    metricSelectorMode = 'tree';
    metricSelector = new Ext.tree.TreePanel({
      root: new Ext.tree.TreeNode({}),
      containerScroll: true,
      autoScroll: true,
      flex: 3.0,
      pathSeparator: '.',
      rootVisible: false,
      singleExpand: false,
      trackMouseOver: true,
      listeners: {
      click: metricTreeSelectorNodeClicked,
      contextmenu: function (node, e) {
                     if (!node.leaf) {
                       folderContextMenu.node = node;
                       folderContextMenu.showAt( e.getXY() );
                     }
                   }
      }
    });
  } else { // NAV_BAR_REGION == 'north'
    metricSelectorMode = 'text';
    metricSelectorGrid = new Ext.grid.GridPanel({
      region: 'center',
      hideHeaders: true,
      loadMask: true,
      bodyCssClass: 'metric-result',

      colModel: new Ext.grid.ColumnModel({
        defaults: {
          sortable: false,
          menuDisabled: true
        },
        columns: [
          {header: 'Metric Path', width: 1.0, dataIndex: 'path'}
        ]
      }),
      viewConfig: {
        forceFit: true,
        rowOverCls: '',
        bodyCssClass: 'metric-result',
        getRowClass: function(record, index) {
          var toggledClass = (
             graphStore.findExact('target', 'target=' + record.data.path) == -1
            ) ? 'metric-not-toggled' : 'metric-toggled';
          var branchClass = (
            record.data['is_leaf'] == '0'
          ) ? 'result-is-branch-node' : '';
          return toggledClass + ' ' + branchClass + ' metric-result';
        }
      },
      selModel: new Ext.grid.RowSelectionModel({
        singleSelect: false
      }),
      store: new Ext.data.JsonStore({
        method: 'GET',
        url: document.body.dataset.baseUrl + 'metrics/find/',
        autoLoad: true,
        baseParams: {
          query: '*',
          format: 'completer',
          automatic_variants: (UI_CONFIG.automatic_variants) ? '1' : '0'
        },
        fields: ['path', 'is_leaf'],
        root: 'metrics'
      }),
      listeners: {
        rowclick: function (thisGrid, rowIndex, e) {
                    var record = thisGrid.getStore().getAt(rowIndex);
                    if (record.data['is_leaf'] == '1') {
                      graphAreaToggle(record.data.path);
                      thisGrid.getView().refresh();
                    } else {
                      metricSelectorTextField.setValue(record.data.path);
                    }
                    autocompleteTask.delay(50);
                    focusCompleter();
                  }
      }
    });

    function completerKeyPress(thisField, e) {
      var charCode = e.getCharCode();
      if (charCode == 8 ||  //backspace
          charCode >= 46 || //delete and all printables
          charCode == 36 || //home
          charCode == 35) { //end
        autocompleteTask.delay(AUTOCOMPLETE_DELAY);
      }
    }

    metricSelectorTextField = new Ext.form.TextField({
      region: 'south',
      enableKeyEvents: true,
      cls: 'completer-input-field',
      listeners: {
        keypress: completerKeyPress,
        specialkey: completerKeyPress,
        afterrender: focusCompleter
      }
    });
    metricSelector = new Ext.Panel({
      flex: 1.5,
      layout: 'border',
      items: [metricSelectorGrid, metricSelectorTextField]
    });
  }

  var autocompleteTask = new Ext.util.DelayedTask(function () {
    var query = metricSelectorTextField.getValue();
    var store = metricSelectorGrid.getStore();
    if (query === '') {
      query = '*'
    }
    store.setBaseParam('query', query);
    store.load();
  });

  var graphTemplate = new Ext.XTemplate(
    '<tpl for=".">',
      '<div class="graph-container">',
        '<div class="graph-overlay">',
          '<img class="graph-img{loading}" src="{url}" width="{width}" height="{height}" id="graph{index}">',
          '<div class="overlay-close-button" onclick="javascript: graphStore.removeAt(\'{index}\'); updateGraphRecords(); justClosedGraph = true;">X</div>',
        '</div>',
      '</div>',
    '</tpl>',
    '<div class="x-clear"></div>'
  );

  function setupGraphDD () {
    graphView.dragZone = new Ext.dd.DragZone(graphView.getEl(), {
      containerScroll: true,
      ddGroup: 'graphs',

      getDragData: function (e) {
        var sourceEl = e.getTarget(graphView.itemSelector, 10);
        if (sourceEl) {
          var dupe = sourceEl.cloneNode(true);
          dupe.id = Ext.id();
          return {
            ddel: dupe,
            sourceEl: sourceEl,
            repairXY: Ext.fly(sourceEl).getXY(),
            sourceStore: graphStore,
            draggedRecord: graphView.getRecord(sourceEl)
          }
        }
      },

      getRepairXY: function () {
        return this.dragData.repairXY;
      }

    });

    graphView.dropZone = new Ext.dd.DropZone(graphView.getEl(), {
      ddGroup: 'graphs',
      dropAction: 'reorder',
      mergeEl: Ext.get('merge'),

      getTargetFromEvent: function (e) {
        return e.getTarget(graphView.itemSelector);
      },

      onNodeEnter: function (target, dd, e, data) {
        //Ext.fly(target).addClass('graph-highlight');
        this.setDropAction('reorder');
        this.mergeTarget = Ext.get(target);
        this.mergeSwitchTimeout = this.setDropAction.defer(UI_CONFIG.merge_hover_delay, this, ['merge']);
      },

      onNodeOut: function (target, dd, e, data) {
        //Ext.fly(target).removeClass('graph-highlight');
        this.mergeEl.hide();
        //this.setDropAction('reorder');
      },

      onNodeOver: function (target, dd, e, data) {
        return Ext.dd.DropZone.prototype.dropAllowed;
      },

      setDropAction: function (action) {
        if (this.mergeSwitchTimeout != null) {
          clearTimeout(this.mergeSwitchTimeout);
          this.mergeSwitchTimeout = null;
        }

        this.dropAction = action;
        if (action == 'reorder') {
          //revert merge ui change
          this.mergeEl.hide();
        } else if (action == 'merge') {
          //apply merge ui change
          this.mergeEl.show();
          var targetXY = this.mergeTarget.getXY();
          var mergeElWidth = Math.max(GraphSize.width * 0.75, 20);
          var xOffset = (GraphSize.width - mergeElWidth) / 2;
          var yOffset = -14;
          this.mergeEl.setXY([targetXY[0] + xOffset, targetXY[1] + yOffset]);
          this.mergeEl.setWidth(mergeElWidth);
        }
      },

      onNodeDrop: function (target, dd, e, data){
        var nodes = graphView.getNodes();
        var dropIndex = nodes.indexOf(target);
        var dragIndex = graphStore.indexOf(data.draggedRecord);

        if (dragIndex == dropIndex) {
          return false;
        }

        if (this.dropAction == 'reorder') {
          graphStore.removeAt(dragIndex);
          graphStore.insert(dropIndex, data.draggedRecord);
          updateGraphRecords();
          return true;
        } else if (this.dropAction == 'merge') {
          var dragRecord = data.draggedRecord;
          var dropRecord = graphView.getRecord(target);
          if (dropRecord.data.params.target.length == 1) {
            if (dropRecord.data.params.target[0] == dropRecord.data.params.title) {
              delete dropRecord.data.params.title;
            }
          }

          var mergedTargets = uniq( dragRecord.data.params.target.concat(dropRecord.data.params.target) );
          dropRecord.data.params.target = mergedTargets;
          dropRecord.data.target = Ext.urlEncode({target: mergedTargets});
          dropRecord.commit();
          graphStore.remove(dragRecord);
          updateGraphRecords();
          return true;
        }
        return false;
      }
    });
  }

  graphView = new Ext.DataView({
    store: graphStore,
    tpl: graphTemplate,
    overClass: 'graph-over',
    itemSelector: 'div.graph-container',
    emptyText: 'Configure your context above, and then select some metrics.',
    autoScroll: true,
//    plugins: [
//      new Ext.ux.DataViewTransition({
//        duration: 750,
//        idProperty: 'target'
//      })
//    ],
    listeners: {
      click: graphClicked,
      render: setupGraphDD
    }
  });

  /* Toolbar items */
  var relativeTimeRange = {
          icon: CLOCK_ICON,
          text: 'Relative Time Range',
          tooltip: 'View Recent Data',
          handler: selectRelativeTime,
          scope: this
  };

  var absoluteTimeRange = {
    icon: CALENDAR_ICON,
    text: 'Absolute Time Range',
    tooltip: 'View Specific Time Range',
    handler: selectAbsoluteTime,
    scope: this
  };

  var timeRangeText = {
    id: 'time-range-text',
    xtype: 'tbtext',
    text: getTimeText()
  };

  // Note that some of these items are changed in postLoginMenuAdjust() after login/logout
  var dashboardMenu = {
    text: 'Dashboard',
    menu: {
      items: [
        {
          text: 'New',
          handler: function (item, e) {
                     setDashboardName(null);
                     if (NEW_DASHBOARD_REMOVE_GRAPHS) {
                       graphStore.removeAll();
                     }
                     refreshGraphs();
                   }
        }, {
          text: 'Finder',
          handler: showDashboardFinder
        }, {
          text: 'Template Finder',
          handler: showTemplateFinder
        }, {
          text: 'Save As Template',
          handler: saveTemplate,
          disabled: !hasPermission('change')
        }, {
          id: 'dashboard-save-button',
          text: 'Save',
          handler: function (item, e) {
                     sendSaveRequest(dashboardName);
                   },
          disabled: dashboardName == null || !hasPermission('change')
        }, {
          id: 'dashboard-save-as-button',
          text: 'Save As',
          handler: saveDashboard,
          disabled: !hasPermission('change')
        }, {
          text: 'Configure UI',
          handler: configureUI
        }, {
          text: 'Edit Dashboard',
          handler: editDashboard
        }, {
          id: 'dashboard-login-button',
          text: getLoginMenuItemText(),
          handler: function (item, e) {
                     if (isLoggedIn()) {
                       logout();
                     } else {
                       showLoginForm();
                     }
                   }
        }
      ]
    }
  };

  var graphsMenu = {
    text: 'Graphs',
    menu: {
      items: [
        { text: 'New Graph',
          menu: {
            items: [
              { text: 'Empty Graph',
                handler: newEmptyGraph
              },
              { text: 'From URL',
                handler: newFromUrl
              },
              { text: 'From Saved Graph',
                handler: newFromSavedGraph
              },
              { text: 'From Metric',
                handler: newFromMetric
              }
            ]
          }
        },
        {
          text: 'Edit Default Parameters',
          handler: editDefaultGraphParameters
        }, {
          text: 'Resize',
          handler: selectGraphSize
        }, {
          text: 'Remove All',
          handler: removeAllGraphs
        }
      ]
    }
  };

  var shareButton = {
    icon: SHARE_ICON,
    tooltip: 'Share This Dashboard',
    text: 'Share',
    handler: doShare
  };

  var helpButton = {
    icon: HELP_ICON,
    tooltip: 'Keyboard Shortcuts',
    handler: showHelp
  };

  var resizeButton = {
    icon: RESIZE_ICON,
    tooltip: 'Resize Graphs',
    handler: selectGraphSize
  };

  var removeAllButton = {
    icon: REMOVE_ICON,
    tooltip: 'Remove All Graphs',
    handler: removeAllGraphs
  };

  var refreshButton = {
    icon: REFRESH_ICON,
    tooltip: 'Refresh Graphs',
    handler: refreshGraphs
  };

  var autoRefreshButton = {
    xtype: 'button',
    id: 'auto-refresh-button',
    text: 'Auto-Refresh',
    enableToggle: true,
    pressed: false,
    tooltip: 'Toggle auto-refresh',
    toggleHandler: function (button, pressed) {
                     if (pressed) {
                       startTask(refreshTask);
                     } else {
                       stopTask(refreshTask);
                     }
                   }
  };

  var every = {
    xtype: 'tbtext',
    text: 'every'
  };

  var seconds = {
    xtype: 'tbtext',
    text: 'seconds'
  };

  var autoRefreshField = {
    id: 'auto-refresh-field',
    xtype: 'textfield',
    width: 25,
    value: UI_CONFIG.refresh_interval,
    enableKeyEvents: true,
    disableKeyFilter: true,
    listeners: {
      change: function (field, newValue) { updateAutoRefresh(newValue); },
      specialkey: function (field, e) {
                    if (e.getKey() == e.ENTER) {
                      if (field.getValue() >= 1) {
                        updateAutoRefresh( field.getValue() );
                      }
                    }
                  }
    }
  };

  var lastRefreshed = {
    xtype: 'tbtext',
    text: 'Last Refreshed: '
  };

  var lastRefreshedText = {
    id: 'last-refreshed-text',
    xtype: 'tbtext',
    text: ( new Date() ).format('g:i:s A')
  };

  graphArea = new Ext.Panel({
    region: 'center',
    layout: 'fit',
    autoScroll: false,
    bodyCssClass: 'graph-area-body',
    items: [graphView],
    tbar: new Ext.Toolbar({
      items: [
        dashboardMenu,
        graphsMenu,
        '-',
        shareButton,
        '-',
        relativeTimeRange,
        absoluteTimeRange,
        ' ',
        timeRangeText,
        '->',
        helpButton,
        resizeButton,
        removeAllButton,
        refreshButton,
        autoRefreshButton,
        every, autoRefreshField, seconds,
        '-',
        lastRefreshed, lastRefreshedText
      ]
    })
  });

  /* Nav Bar */
  navBarNorthConfig.items = [metricSelector];
  navBarWestConfig.items = [contextSelector, metricSelector];
  var navBarConfig = (NAV_BAR_REGION == 'north') ? navBarNorthConfig : navBarWestConfig;
  navBar = new Ext.Panel(navBarConfig);

  viewport = new Ext.Viewport({
    layout: 'border',
    items: [
      navBar,
      graphArea
    ]
  });

  refreshTask = {
    run: refreshGraphs,
    interval: UI_CONFIG.refresh_interval * 1000
  };

  // Load initial dashboard state if it was passed in
  if (initialState) {
    applyState(initialState);
    navBar.collapse(false);
  }

  if(window.location.hash != '')
  {
    if (window.location.hash.indexOf('/') != -1) {
      var nameVal = window.location.hash.substr(1).split('/');
      sendLoadTemplateRequest(nameVal[0],nameVal[1]);
    } else {
      sendLoadRequest(window.location.hash.substr(1));
    }
    navBar.collapse(false);
  }

  if (initialError) {
    Ext.Msg.alert('Error', htmlEncode(initialError));
  }
}

function showHelp() {
  var win = new Ext.Window({
    title: 'Keyboard Shortcuts',
    modal: true,
    width: 550,
    height: 300,
    autoLoad: document.body.dataset.baseUrl + 'dashboard/help/'
  });
  win.show();
}

function metricTypeSelected (combo, record, index) {
  selectedScheme = record;

  // Show only the fields for the selected context
  Ext.each(contextSelectorFields, function (field) {
    if (field.getId().indexOf( selectedScheme.get('name') ) == 0) {
      field.show();
    } else {
      field.hide();
    }
  });

  setContextFieldCookie('metric-type', combo.getValue());
  contextFieldChanged();
  focusCompleter();
}


function buildQuery (queryEvent) {
  var queryString = '';
  var parts = selectedScheme.get('pattern').split('.');
  var schemeName = selectedScheme.get('name');

  // Clear cached records to force JSON queries every time
  contextFieldStore.removeAll();
  delete queryEvent.combo.lastQuery;

  for (var i = 0; i < parts.length; i++) {
    var part = parts[i];
    var field = part.match(/^<[^>]+>$/) ? part.substr(1, part.length - 2) : null;

    if (field == null) {
      queryString += part + '.';
      continue;
    }

    var combo = Ext.getCmp(schemeName + '-' + field);
    var value = combo.getValue();

    if (UI_CONFIG.automatic_variants) {
      if (value.indexOf(',') > -1 && value.search(/[{}]/) == -1) {
        value = '{' + value + '}';
      }
    }

    if (combo === queryEvent.combo) {
      queryEvent.query = queryString + queryEvent.query + '*';
      return;
    } else {
      if (value) {
        queryString += value + '.';
      } else {
        Ext.Msg.alert('Missing Context', 'Please fill out all of the fields above first.');
        queryEvent.cancel = true;
        return;
      }
    }
  }

  Ext.Msg.alert('Error', htmlEncode('Failed to build query, could not find "' + queryEvent.combo.getId() + '" field'));
  queryEvent.cancel = true;
}


function contextFieldChanged() {
  var pattern = getContextFieldsPattern();
  if (pattern) metricSelectorShow(pattern);
}

function getContextFieldsPattern() {
  var schemeName = selectedScheme.get('name');
  var pattern = selectedScheme.get('pattern');
  var fields = selectedScheme.get('fields');
  var missingFields = false;

  Ext.each(fields, function (field) {
    var id = schemeName + '-' + field.name;
    var value = Ext.getCmp(id).getValue();

    // Update context field cookies
    setContextFieldCookie(field.name, value);

    if (UI_CONFIG.automatic_variants) {
      if (value.indexOf(',') > -1 && value.search(/[{}]/) == -1) {
        value = '{' + value + '}';
      }
    }

    if (value.trim() == '') {
      missingFields = true;
    } else {
      pattern = pattern.replace('<' + field.name + '>', value);
    }
  });

  if (missingFields) {
    return;
  }

  return pattern;
}

function metricSelectorShow(pattern) {
  if (metricSelectorMode == 'tree') {
    metricTreeSelectorShow(pattern);
  } else {
    metricTextSelectorShow(pattern);
  }
}

function metricTreeSelectorShow(pattern) {
  var baseParts = pattern.split('.');

  function setParams (loader, node, callback) {
    loader.baseParams.format = 'treejson';

    if (node.id == 'rootMetricSelectorNode') {
      loader.baseParams.query = pattern + '.*';
    } else {
      var idParts = node.id.split('.');
      idParts.splice(0, baseParts.length); //make it relative
      var relativeId = idParts.join('.');
      loader.baseParams.query = pattern + '.' + relativeId + '.*';
    }
  }

  var loader = new Ext.tree.TreeLoader({
    url: document.body.dataset.baseUrl + 'metrics/find/',
    requestMethod: 'GET',
    listeners: {beforeload: setParams}
  });

  try {
    var oldRoot = Ext.getCmp('rootMetricSelectorNode');
    oldRoot.destroy();
  } catch (err) { }

  var root = new Ext.tree.AsyncTreeNode({
    id: 'rootMetricSelectorNode',
    loader: loader
  });

  metricSelector.setRootNode(root);
  root.expand();
}

function metricTextSelectorShow(pattern) {
  var store = metricSelectorGrid.getStore();
  store.setBaseParam('query', pattern);
  store.load();
}


function metricTreeSelectorNodeClicked (node, e) {
  if (!node.leaf) {
    if (node.expanded) {
      node.collapse();
    } else {
      node.loaded = false;
      node.expand();
    }
    return;
  }

  graphAreaToggle(node.id);
}


function graphAreaToggle(target, options) {
  /* The GraphRecord's id is their URL-encoded target=...&target=... string
     This function can get called with either the encoded string or just a raw
     metric path, eg. "foo.bar.baz".
  */
  var graphTargetString;
  if (target.substr(0,7) == 'target=') {
    graphTargetString = target;
  } else {
    graphTargetString = 'target=' + target;
  }
  var graphTargetList = Ext.urlDecode(graphTargetString)['target'];
  if (typeof graphTargetList == 'string') {
    graphTargetList = [graphTargetList];
  }

  var existingIndex = graphStore.findExact('target', graphTargetString);

  if (existingIndex > -1) {
    if ( (options === undefined) || (!options.dontRemove) ) {
      graphStore.removeAt(existingIndex);
    }
  } else if ( (options === undefined) || (!options.onlyRemove) ) {
    // Add it
    var myParams = {
      target: graphTargetList
    };
    var urlParams = {};
    Ext.apply(urlParams, defaultGraphParams);
    if (options && options.defaultParams) {
      Ext.apply(urlParams, options.defaultParams);
    }
    Ext.apply(urlParams, GraphSize);
    Ext.apply(urlParams, myParams);

    var record = new GraphRecord({
      target: graphTargetString,
      params: myParams,
      url: document.body.dataset.baseUrl + 'render?' + Ext.urlEncode(urlParams)
    });
    graphStore.add([record]);
    updateGraphRecords();
  }
}

function importGraphUrl(targetUrl, options) {
  var fullUrl = targetUrl;
  var i = fullUrl.indexOf('?');
  if (i == -1) {
    return;
  }

  var queryString = fullUrl.substr(i+1);
  var params = Ext.urlDecode(queryString);

  var graphTargetList = params['target'];
  if (typeof graphTargetList == 'string') {
    graphTargetList = [graphTargetList];
  }
  params['target'] = graphTargetList;

  if (graphTargetList.length == 0) {
    return;
  }

  var graphTargetString = Ext.urlEncode({target: graphTargetList});
  var existingIndex = graphStore.findExact('target', graphTargetString);

  if (existingIndex > -1) {
    if ( (options === undefined) || (!options.dontRemove) ) {
      graphStore.removeAt(existingIndex);
    }
  } else {
    var urlParams = {};
    Ext.apply(urlParams, defaultGraphParams);
    Ext.apply(urlParams, params);
    Ext.apply(urlParams, GraphSize);

    var record = new GraphRecord({
      target: graphTargetString,
      params: params,
      url: document.body.dataset.baseUrl + 'render?' + Ext.urlEncode(urlParams)
      });
      graphStore.add([record]);
      updateGraphRecords();
  }
}

function updateGraphRecords() {
  graphStore.each(function (item, index) {
    var params = {};
    Ext.apply(params, defaultGraphParams);
    Ext.apply(params, item.data.params);
    Ext.apply(params, GraphSize);
    params._uniq = Math.random();
    if (params.title === undefined && params.target.length == 1) {
      params.title = params.target[0];
    }
    if (!params.uniq === undefined) {
        delete params['uniq'];
    }

    //Preload the image and set it into the UI once it is available.
    item.set('loading','-loading');
    var img = new Image();
    img.onload = function() {
      item.set('url',img.src);
      item.set('loading','');
    };
    img.src = document.body.dataset.baseUrl + 'render?' + Ext.urlEncode(params);

    item.set('width', GraphSize.width);
    item.set('height', GraphSize.height);
    item.set('index', index);
  });
}

function refreshGraphs() {
  updateGraphRecords();
  graphView.refresh();
  graphArea.getTopToolbar().get('last-refreshed-text').setText( (new Date()).format('g:i:s A') );
}

/*
function refreshGraph(index) {
  var node = graphView.getNode(index);
  var record = graphView.getRecord(node);
  record.data.params.uniq = Math.random();
  record.set('url', '/render?' + Ext.urlEncode(record.get('params')));

  // This refreshNode method only refreshes the record data, it doesn't re-render
  // the template. Which is pretty useless... It would be more efficient if we
  // could simply re-render the template. Need to see if thats feasible.
  //graphView.refreshNode(node);

  // This is *slightly* better than just calling refreshGraphs() because we're only
  // updating the URL of one graph, so caching should save us from re-rendering each
  // graph.
  //graphView.refresh();
}
*/

function updateAutoRefresh (newValue) {
  Ext.getCmp('auto-refresh-field').setValue(newValue);

  var value = parseInt(newValue);
  if ( isNaN(value) ) {
    return;
  }

  if (Ext.getCmp('auto-refresh-button').pressed) {
    stopTask(refreshTask);
    refreshTask.interval = value * 1000;
    startTask(refreshTask);
  } else {
    refreshTask.interval = value * 1000;
  }
}

/* Task management */
function stopTask(task) {
  if (task.running) {
    Ext.TaskMgr.stop(task);
    task.running = false;
  }
}

function startTask(task) {
  if (!task.running) {
    Ext.TaskMgr.start(task);
    task.running = true;
  }
}

/* Time Range management */
defaultGraphParams['from'].match(/([0-9]+)([^0-9]+)/);
var defaultRelativeQuantity = RegExp.$1;
var defaultRelativeUnits = RegExp.$2;
var TimeRange = {
  // Default to a relative time range
  type: 'relative',
  relativeStartQuantity: defaultRelativeQuantity,
  relativeStartUnits: defaultRelativeUnits,
  relativeUntilQuantity: '',
  relativeUntilUnits: 'now',
  // Absolute time range
  startDate: new Date(),
  startTime: '9:00 AM',
  endDate: new Date(),
  endTime: '5:00 PM'
};

function getTimeText() {
  if (TimeRange.type == 'relative') {
    var text = 'Now showing the past ' + TimeRange.relativeStartQuantity + ' ' + TimeRange.relativeStartUnits;
    if (TimeRange.relativeUntilUnits !== 'now' && TimeRange.relativeUntilUnits !== '') {
      text = text + ' until ' + TimeRange.relativeUntilQuantity + ' ' + TimeRange.relativeUntilUnits + ' ago';
    }
    return text;
  } else {
    var fmt = 'g:ia F jS Y';
    return 'Now Showing ' + TimeRange.startDate.format(fmt) + ' through ' + TimeRange.endDate.format(fmt);
  }
}

function updateTimeText() {
  graphArea.getTopToolbar().get('time-range-text').setText( getTimeText() );
}

function timeRangeUpdated() {
  if (TimeRange.type == 'relative') {
    var fromParam = '-' + TimeRange.relativeStartQuantity + TimeRange.relativeStartUnits;
    if (TimeRange.relativeUntilUnits == 'now') {
      var untilParam = 'now';
    } else {
      var untilParam = '-' + TimeRange.relativeUntilQuantity + TimeRange.relativeUntilUnits;
    }
  } else {
    var fromParam = TimeRange.startDate.format('H:i_Ymd');
    var untilParam = TimeRange.endDate.format('H:i_Ymd');
  }
  defaultGraphParams.from = fromParam;
  defaultGraphParams.until = untilParam;
  saveDefaultGraphParams();

  graphStore.each(function () {
    this.data.params.from = fromParam;
    this.data.params.until = untilParam;
  });

  updateTimeText();
  refreshGraphs();
}


function selectRelativeTime() {
  var quantityField = new Ext.form.TextField({
    fieldLabel: 'Show the past',
    width: 90,
    allowBlank: false,
    regex: /\d+/,
    regexText: 'Please enter a number',
    value: TimeRange.relativeStartQuantity
  });

  var unitField = new Ext.form.ComboBox({
    fieldLabel: '',
    width: 90,
    mode: 'local',
    editable: false,
    triggerAction: 'all',
    allowBlank: false,
    forceSelection: true,
    store: ['minutes', 'hours', 'days', 'weeks', 'months'],
    value: TimeRange.relativeStartUnits
  });

  var untilQuantityField = new Ext.form.TextField({
    id: 'until-quantity-field',
    fieldLabel: 'Until',
    width: 90,
    allowBlank: true,
    regex: /\d+/,
    regexText: 'Please enter a number',
    value: TimeRange.relativeUntilQuantity
  });

  var untilUnitField = new Ext.form.ComboBox({
    fieldLabel: '',
    width: 90,
    mode: 'local',
    editable: false,
    triggerAction: 'all',
    allowBlank: true,
    forceSelection: false,
    store: ['now', 'minutes', 'hours', 'days', 'weeks', 'months'],
    value: TimeRange.relativeUntilUnits,
    listeners: {
      select: function(combo, record, index) {
                  if (index == 0) {
                    Ext.getCmp('until-quantity-field').setValue('');
                    Ext.getCmp('until-quantity-field').setDisabled(true);
                  } else {
                    Ext.getCmp('until-quantity-field').setDisabled(false);
                  }
                },
      render: function(combo) {
                if (combo.getValue() == 'now') {
                  Ext.getCmp('until-quantity-field').setValue('');
                  Ext.getCmp('until-quantity-field').setDisabled(true);
                } else {
                  Ext.getCmp('until-quantity-field').setDisabled(false);
                }
              }
    }
  });


  var win;

  function updateTimeRange() {
    TimeRange.type = 'relative';
    TimeRange.relativeStartQuantity = quantityField.getValue();
    TimeRange.relativeStartUnits = unitField.getValue();
    TimeRange.relativeUntilQuantity = untilQuantityField.getValue();
    TimeRange.relativeUntilUnits = untilUnitField.getValue();
    win.close();
    timeRangeUpdated();
  }

  win = new Ext.Window({
    title: 'Select Relative Time Range',
    width: 205,
    height: 170,
    resizable: false,
    modal: true,
    layout: 'form',
    labelAlign: 'right',
    labelWidth: 90,
    items: [quantityField, unitField, untilQuantityField, untilUnitField],
    buttonAlign: 'center',
    buttons: [
      {text: 'Ok', handler: updateTimeRange},
      {text: 'Cancel', handler: function () { win.close(); } }
    ]
  });
  win.show();
}

function selectAbsoluteTime() {
  var startDateField = new Ext.form.DateField({
    fieldLabel: 'Start Date',
    width: 125,
    value: TimeRange.startDate || ''
  });

  var startTimeField = new Ext.form.TimeField({
    fieldLabel: 'Start Time',
    width: 125,
    allowBlank: false,
    increment: 30,
    value: TimeRange.startTime || ''
  });

  var endDateField = new Ext.form.DateField({
    fieldLabel: 'End Date',
    width: 125,
    value: TimeRange.endDate || ''
  });

  var endTimeField = new Ext.form.TimeField({
    fieldLabel: 'End Time',
    width: 125,
    allowBlank: false,
    increment: 30,
    value: TimeRange.endTime || ''
  });

  var win;

  function updateTimeRange() {
    TimeRange.type = 'absolute';
    TimeRange.startDate = new Date(startDateField.getValue().format('Y/m/d ') + startTimeField.getValue());
    TimeRange.startTime = startTimeField.getValue();
    TimeRange.endDate = new Date(endDateField.getValue().format('Y/m/d ') + endTimeField.getValue());
    TimeRange.endTime = endTimeField.getValue();
    win.close();
    timeRangeUpdated();
  }

  win = new Ext.Window({
    title: 'Select Absolute Time Range',
    width: 225,
    height: 180,
    resizable: false,
    modal: true,
    layout: 'form',
    labelAlign: 'right',
    labelWidth: 70,
    items: [startDateField, startTimeField, endDateField, endTimeField],
    buttonAlign: 'center',
    buttons: [
      {text: 'Ok', handler: updateTimeRange},
      {text: 'Cancel', handler: function () { win.close(); } }
    ]
  });
  win.show();
}


/* Graph size stuff */
var GraphSize = {
  width: UI_CONFIG.default_graph_width,
  height: UI_CONFIG.default_graph_height
};


// New empty Graph
function newEmptyGraph() {

  var myParams = {
    target: []
  };

  var graphTargetString = Ext.urlEncode({target: ''});

  var urlParams = {};
  Ext.apply(urlParams, defaultGraphParams);
  Ext.apply(urlParams, myParams);
  Ext.apply(urlParams, GraphSize);
  myParams['from'] = urlParams.from;
  myParams['until'] = urlParams.until;

  var record = new GraphRecord({
   target: graphTargetString,
    params: myParams,
    url: document.body.dataset.baseUrl + 'render?' + Ext.urlEncode(urlParams),
   'width': GraphSize.width,
   'height': GraphSize.height,
    });
  graphStore.add([record]);
  var canvasId = graphStore.indexOf(record);
  graphStore.getAt(canvasId).data.index = canvasId;
  updateGraphRecords();
}

function newFromUrl() {
  function applyUrl() {
    var inputUrl = Ext.getCmp('import-url-field').getValue();
    importGraphUrl(inputUrl);
    win.close();
  }

  var urlField = new Ext.form.TextField({
    id: 'import-url-field',
    fieldLabel: 'Graph URL',
    region: 'center',
    width: '100%',
    listeners: {
      specialkey: function (field, e) {
                    if (e.getKey() == e.ENTER) {
                      applyUrl();
                    }
                  },
      afterrender: function (field) { field.focus(false, 100); }
    }
  });

  var win = new Ext.Window({
    title: 'Import Graph From URL',
    width: 470,
    height: 87,
    layout: 'form',
    resizable: true,
    modal: true,
    items: [urlField],
    buttonAlign: 'center',
    buttons: [
      {
        text: 'OK',
        handler: applyUrl
      }, {
        text: 'Cancel',
        handler: function () { win.close(); }
      }
    ]
  });
  win.show();

}

function newFromSavedGraph() {
  function setParams(loader, node) {
    var nodeId = node.id.replace(/^[A-Za-z]+Tree\.?/,'');
    loader.baseParams.query = (nodeId == '') ? '*' : (nodeId + '.*');
    loader.baseParams.format = 'treejson';
    loader.baseParams.contexts = '1';
    loader.baseParams.path = nodeId;
    if (node.parentNode && node.parentNode.id == 'UserGraphsTree') {
      loader.baseParams.user = node.id;
    }
  }

  var userGraphsNode = new Ext.tree.AsyncTreeNode({
    id: 'UserGraphsTree',
    leaf: false,
    allowChildren: true,
    expandable: true,
    allowDrag: false,
    loader: new Ext.tree.TreeLoader({
      url: document.body.dataset.baseUrl + 'browser/usergraph/',
      requestMethod: 'GET',
      listeners: {beforeload: setParams}
    })
  });

  function handleSelects(selModel, nodes) {
    Ext.each(nodes, function (node, index) {
      if (!node.leaf) {
        node.unselect();
        node.toggle();
      }
    });

    if (selModel.getSelectedNodes().length == 0) {
      Ext.getCmp('user-graphs-select-button').setDisabled(true);
    } else {
      Ext.getCmp('user-graphs-select-button').setDisabled(false);
    }
  }

  var treePanel = new Ext.tree.TreePanel({
    id: 'user-graphs-tree',
    header: false,
    region: 'center',
    root: userGraphsNode,
    containerScroll: true,
    autoScroll: true,
    pathSeparator: '.',
    rootVisible: false,
    singleExpand: false,
    trackMouseOver: true,
    selModel: new Ext.tree.MultiSelectionModel({
      listeners: {
        selectionchange: handleSelects
      }
    })
  });

  function selectUserGraphs(selectedNodes) {
    Ext.each(selectedNodes, function (node, index) {
      importGraphUrl(node.attributes.graphUrl);
    });
  }

  var win = new Ext.Window({
    title: 'Import From User Graphs',
    width: 300,
    height: 400,
    layout: 'border',
    resizable: true,
    modal: true,
    items: [treePanel],
    buttonAlign: 'center',
    buttons: [
      {
        id: 'user-graphs-select-button',
        text: 'Select',
        disabled: true,
        handler: function () {
          selectUserGraphs(Ext.getCmp('user-graphs-tree').getSelectionModel().getSelectedNodes());
          win.close();
        }
      }, {
        text: 'Cancel',
        handler: function () { win.close(); }
      }
    ]
  });
  win.show();
}

function newFromMetric() {
  function applyMetric() {
    var inputMetric = Ext.getCmp('import-metric-field').getValue();
    if (inputMetric == '') {
      return;
    }
    var graphTargetString = Ext.urlEncode({target: inputMetric});

    var myParams = {
      target: [inputMetric]
    };

    var urlParams = {};
    Ext.apply(urlParams, defaultGraphParams);
    Ext.apply(urlParams, myParams);
    Ext.apply(urlParams, GraphSize);

    var record = new GraphRecord({
      target: graphTargetString,
      params: myParams,
      url: document.body.dataset.baseUrl + 'render?' + Ext.urlEncode(urlParams)
      });
    graphStore.add([record]);
    updateGraphRecords();
    win.close();
  }

  var urlField = new Ext.form.TextField({
    id: 'import-metric-field',
    fieldLabel: 'Metric',
    region: 'center',
    width: '100%',
    listeners: {
      specialkey: function (field, e) {
                    if (e.getKey() == e.ENTER) {
                      applyMetric();
                    }
                  },
      afterrender: function (field) { field.focus(false, 100); }
    }
  });

  var win = new Ext.Window({
    title: 'Import Graph From Metric',
    width: 470,
    height: 87,
    layout: 'form',
    resizable: true,
    modal: true,
    items: [urlField],
    buttonAlign: 'center',
    buttons: [
      {
        text: 'OK',
        handler: applyMetric
      }, {
        text: 'Cancel',
        handler: function () { win.close(); }
      }
    ]
  });
  win.show();
}


function editDefaultGraphParameters() {
  var editParams = Ext.apply({}, defaultGraphParams);
  removeUneditable(editParams);

  function applyParams() {
    var paramsString = Ext.getCmp('default-params-field').getValue();
    var params = Ext.urlDecode(paramsString);
    copyUneditable(defaultGraphParams, params);
    defaultGraphParams = params;
    saveDefaultGraphParams();
    refreshGraphs();
    win.close();
  }

  var paramsField = new Ext.form.TextField({
    id: 'default-params-field',
    region: 'center',
    value: Ext.urlEncode(editParams),
    listeners: {
      specialkey: function (field, e) {
                    if (e.getKey() == e.ENTER) {
                      applyParams();
                    }
                  },
      afterrender: function (field) { field.focus(false, 100); }
    }
  });

  var win = new Ext.Window({
    title: 'Default Graph Parameters',
    width: 470,
    height: 87,
    layout: 'border',
    resizable: true,
    modal: true,
    items: [paramsField],
    buttonAlign: 'center',
    buttons: [
      {
        text: 'OK',
        handler: applyParams
      }, {
        text: 'Cancel',
        handler: function () { win.close(); }
      }
    ]
  });
  win.show();
}

function selectGraphSize() {
  var presetCombo = new Ext.form.ComboBox({
    fieldLabel: 'Preset',
    width: 80,
    editable: false,
    forceSelection: true,
    triggerAction: 'all',
    mode: 'local',
    value: 'Custom',
    store: ['Custom', 'Small', 'Medium', 'Large'],
    listeners: {
      select: function (combo, record, index) {
                var w = '';
                var h = '';
                if (index == 1) { //small
                  w = 300;
                  h = 230;
                } else if (index == 2) { //medium
                  w = 400;
                  h = 300;
                } else if (index == 3) { //large
                  w = 500;
                  h = 400;
                }
                Ext.getCmp('width-field').setValue(w);
                Ext.getCmp('height-field').setValue(h);
              }
    }
  });

  var widthField = new Ext.form.TextField({
    id: 'width-field',
    fieldLabel: 'Width',
    width: 80,
    regex: /\d+/,
    regexText: 'Please enter a number',
    allowBlank: false,
    value: GraphSize.width || UI_CONFIG.default_graph_width
  });

  var heightField = new Ext.form.TextField({
    id: 'height-field',
    fieldLabel: 'Height',
    width: 80,
    regex: /\d+/,
    regexText: 'Please enter a number',
    allowBlank: false,
    value: GraphSize.height || UI_CONFIG.default_graph_height
  })

  var win;

  function resize() {
    GraphSize.width = defaultGraphParams.width = widthField.getValue();
    GraphSize.height = defaultGraphParams.height = heightField.getValue();
    saveDefaultGraphParams();
    win.close();
    refreshGraphs();
  }

  win = new Ext.Window({
    title: 'Change Graph Size',
    width: 185,
    height: 160,
    resizable: false,
    layout: 'form',
    labelAlign: 'right',
    labelWidth: 80,
    modal: true,
    items: [presetCombo, widthField, heightField],
    buttonAlign: 'center',
    buttons: [
      {text: 'Ok', handler: resize},
      {text: 'Cancel', handler: function () { win.close(); } }
    ]
  });
  win.show();
}

function doShare() {
  if (dashboardName == null) {
    Ext.Ajax.request({
      url: document.body.dataset.baseUrl + 'dashboard/create-temporary/',
      method: 'POST',
      params: {
        state: Ext.encode( getState() )
      },
      callback: function (options, success, response) {
                  var result = Ext.decode(response.responseText);
                  if (result.error) {
                    Ext.Msg.alert('Error', htmlEncode('There was an error saving this dashboard: ' + result.error));
                  } else {
                    setDashboardName(result.name);
                    sendSaveRequest(result.name); // Resave the state with the proper dashboardName now
                    showShareWindow();
                  }
                }
    });
  } else {
    // Prompt the user to save their dashboard so they are aware only saved changes get shared
    Ext.Msg.show({
      title: 'Save Dashboard And Share',
      msg: 'You must save changes to your dashboard in order to share it.',
      buttons: Ext.Msg.OKCANCEL,
      fn: function (button) {
            if (button == 'ok') {
              sendSaveRequest(dashboardName);
              showShareWindow();
            }
          }
    });

  }
}

function showShareWindow() {
  var win = new Ext.Window({
    title: 'Share Dashboard',
    width: 600,
    height: 125,
    layout: 'border',
    modal: true,
    items: [
      {
        xtype: 'label',
        region: 'north',
        style: 'text-align: center;',
        text: 'You can use this URL to access the current dashboard.'
      }, {
        xtype: 'textfield',
        region: 'center',
        value: dashboardURL,
        editable: false,
        style: 'text-align: center; font-size: large;',
        listeners: {
          focus: function (field) { field.selectText(); }
        }
      }
    ],
    buttonAlign: 'center',
    buttons: [
      {text: 'Close', handler: function () { win.close(); } }
    ]
  });
  win.show();
}

/* Other stuff */
var targetGrid;
var activeMenu;

function graphClicked(graphView, graphIndex, element, evt) {
  Ext.get('merge').hide();
  var record = graphStore.getAt(graphIndex);
  if (!record) {
    return;
  }

  if (justClosedGraph) {
    justClosedGraph = false;
    return;
  }

  if ( (activeMenu != null) && (selectedRecord == record) ) {
    activeMenu.destroy();
    activeMenu = null;
    return;
  }

  selectedRecord = record; // global state hack for graph options API

  var menu;
  var menuItems = [];

  function applyChanges (field, e) {
    if (e.getKey() != e.ENTER) {
      return;
    }

    var targets = [];
    Ext.each(menuItems, function (field) {
      if ((!field.getXType) || field.getXType() != 'textfield') {
        return;
      }
      if (field.initialConfig.isTargetField) {
        targets.push( field.getValue() );
      } else {
        var newParams = Ext.urlDecode( field.getValue() );
        copyUneditable(record.data.params, newParams);
        record.data.params = newParams;
      }
    });
    record.data.target = Ext.urlEncode( {target: targets} );
    record.data.params.target = targets;

    refreshGraphs();
    menu.destroy();
  }

  function syncGraphs(thisStore, record, operation) {
    var targets = [];
    thisStore.each(function (rec) { targets.push(rec.data.target.replace(/'/g, '"')); });
    selectedRecord.data.params.target = targets;
    selectedRecord.data.target = Ext.urlEncode({target: targets});
    refreshGraphs();
  }


  /* Inline store definition hackery*/
  var functionsButton;
  var targets = record.data.params.target;
  targets = map(targets, function (t) { return {target: t}; });
  var targetStore = new Ext.data.JsonStore({
    fields: ['target'],
    data: targets,
    listeners: {
      update: syncGraphs,
      remove: syncGraphs,
      add: syncGraphs,
    }
  });

  var buttonWidth = 115;
  var rowHeight = 21;
  var maxRows = 6;
  var frameHeight = 5;
  var gridWidth = (buttonWidth * 4) + 2;
  var gridHeight = (rowHeight * Math.min(targets.length, maxRows)) + frameHeight;

  targetGrid = new Ext.grid.EditorGridPanel({
    //frame: true,
    width: gridWidth,
    height: gridHeight,
    store: targetStore,
    hideHeaders: true,
    viewConfig: {
                  markDirty: false,
                  forceFit: true,
                  autoFill: true,
                  scrollOffset: 0
                },
    colModel: new Ext.grid.ColumnModel({
      columns: [
        {
          id: 'target',
          header: 'Target',
          dataIndex: 'target',
          width: gridWidth - 90,
          editor: {xtype: 'textfield'}
        },
        {
            xtype: 'actioncolumn',
            width: 30,
            sortable: false,
            items: [{
                icon: UP_ICON,
                tooltip: 'Move Up',
                handler: function(grid, rowIndex, colIndex) {
                    var record = targetStore.getAt(rowIndex);
                    var target = record.data.target;
                    targetStore.remove(record);
                    if(rowIndex > 0) {
                        targetStore.insert(rowIndex-1, record);
                    } else {
                        targetStore.add(record);
                    }
                }
            }]
        },
        {
            xtype: 'actioncolumn',
            width: 30,
            sortable: false,
            items: [{
                icon: DOWN_ICON,
                tooltip: 'Move Down',
                handler: function(grid, rowIndex, colIndex) {
                    var record = targetStore.getAt(rowIndex);
                    var target = record.data.target;
                    targetStore.remove(record);
                    if(rowIndex < targetStore.getTotalCount()-1) {
                        targetStore.insert(rowIndex+1, record);
                    } else {
                        targetStore.insert(0, record);
                    }
                }
            }]
        },
        {
            xtype: 'actioncolumn',
            width: 30,
            sortable: false,
            items: [{
                icon: TRASH_ICON,
                tooltip: 'Delete Row',
                handler: function(grid, rowIndex, colIndex) {
                    var record = targetStore.getAt(rowIndex);
                    var target = record.data.target;
                    targetStore.remove(record);
                    targets.remove(target);
                }
            }]
        },
      ]
    }),
    selModel: new Ext.grid.RowSelectionModel({
      singleSelect: false,
      listeners: {
        selectionchange: function (thisSelModel) {
          functionsButton.setDisabled(thisSelModel.getCount() == 0);
        }
      }
    }),
    clicksToEdit: 2,
    listeners: {
      afterrender: function (thisGrid) {
        thisGrid.getSelectionModel().selectFirstRow.defer(50, thisGrid.getSelectionModel());
      },
      resize: function (thisGrid) {
        thisGrid.findParentByType('menu').doLayout();
      }
    }
  });
  menuItems.push(targetGrid);

  /* Setup our menus */
  var functionsMenu = new Ext.menu.Menu({
    allowOtherMenus: true,
    items: createFunctionsMenu().concat([ {text: 'Remove Outer Call', handler: removeOuterCall} ])
  });

  functionsButton = new Ext.Button({
    text: 'Apply Function',
    disabled: true,
    width: buttonWidth,
    handler: function (thisButton) {
               if (functionsMenu.isVisible()) {
                 functionsMenu.hide();
               } else {
                 operationsMenu.hide();
                 optionsMenu.doHide(); // private method... yuck
                 functionsMenu.show(thisButton.getEl());
               }
             }
  });


  var optionsMenuConfig = createOptionsMenu(); // defined in composer_widgets.js
  optionsMenuConfig.allowOtherMenus = true;
  var optionsMenu = new Ext.menu.Menu(optionsMenuConfig);
  optionsMenu.on('hide', function () { menu.hide(); });
  updateCheckItems();

  var operationsMenu = new Ext.menu.Menu({
    allowOtherMenus: true,
    items: [{
      xtype: 'button',
      fieldLabel: '<span style=\'visibility: hidden\'>',
      text: 'Breakout',
      width: 100,
      handler: function () { menu.destroy(); breakoutGraph(record); }
    }, {
      xtype: 'button',
      fieldLabel: '<span style=\'visibility: hidden\'>',
      text: 'Clone',
      width: 100,
      handler: function () { menu.destroy(); cloneGraph(record); }
    }, {
      xtype: 'button',
      fieldLabel: '<span style=\'visibility: hidden\'>',
      text: 'Email',
      width: 100,
      handler: function () { menu.destroy(); mailGraph(record); }
    }, {
      xtype: 'button',
      fieldLabel: '<span style=\'visibility: hidden\'>',
      text: 'Direct URL',
      width: 100,
      handler: function () {
        menu.destroy();
        var win = new Ext.Window({
          title: 'Graph URL',
          width: 600,
          height: 125,
          layout: 'border',
          modal: true,
          items: [
            {
              xtype: 'label',
              region: 'north',
              style: 'text-align: center;',
              text: 'Direct URL to this graph'
            }, {
              xtype: 'textfield',
              region: 'center',
              value:  record.data.url,
              editable: false,
              style: 'text-align: center; font-size: large;',
              listeners: {
                focus: function (field) { field.selectText(); }
              }
            }
          ],
          buttonAlign: 'center',
          buttons: [
            {text: 'Close', handler: function () { win.close(); } }
          ]
        });
        win.show();
      },
    }, {
      xtype: 'button',
      fieldLabel: '<span style=\'visibility: hidden\'>',
      text: 'Short Direct URL',
      width: 100,
      handler: function () {
        menu.destroy();
        var showUrl = function(options, success, response) {
            if(success) {
              var win = new Ext.Window({
                title: 'Graph URL',
                width: 600,
                height: 125,
                layout: 'border',
                modal: true,
                items: [
                  {
                    xtype: 'label',
                    region: 'north',
                    style: 'text-align: center;',
                    text: 'Short Direct URL to this graph'
                  }, {
                    xtype: 'textfield',
                    region: 'center',
                    value:  window.location.origin + response.responseText,
                    editable: false,
                    style: 'text-align: center; font-size: large;',
                    listeners: {
                      focus: function (field) { field.selectText(); }
                    }
                  }
                ],
                buttonAlign: 'center',
                buttons: [
                  {text: 'Close', handler: function () { win.close(); } }
                ]
              });
              win.show();
           }
        }
        Ext.Ajax.request({
          method: 'GET',
          url: document.body.dataset.baseUrl + 's' + record.data.url,
          callback: showUrl,
        });
      }
    }, {
        xtype: 'button',
        text: 'History',
        width: 100,
        handler: function () { menu.destroy(); historyGraph(record);}
    }]
  });

  var buttons = [functionsButton];

  buttons.push({
    xtype: 'button',
    text: 'Render Options',
    width: buttonWidth,
    handler: function (thisButton) {
               if (optionsMenu.isVisible()) {
                 optionsMenu.doHide(); // private method... yuck (no other way to hide w/out trigging hide event handler)
               } else {
                 operationsMenu.hide();
                 functionsMenu.hide();
                 optionsMenu.show(thisButton.getEl());
               }
             }
  });

  buttons.push({
    xtype: 'button',
    text: 'Graph Operations',
    width: buttonWidth,
    handler: function (thisButton) {
               if (operationsMenu.isVisible()) {
                 operationsMenu.hide();
               } else {
                 optionsMenu.doHide(); // private method... yuck
                 functionsMenu.hide();
                 operationsMenu.show(thisButton.getEl());
               }
             }
  });

  //create new row
  buttons.push({
    xtype: 'button',
    text: 'Add Target',
    width: buttonWidth,
    handler: function() {
               // Hide the other menus
               operationsMenu.hide();
               optionsMenu.doHide(); // private method... yuck
               functionsMenu.hide();

               targetStore.add([ new targetStore.recordType({target: 'Edit to save'}) ]);
               targets.push('Edit to save');
               targetGrid.setHeight((rowHeight * Math.min(targets.length, maxRows)) + frameHeight);
    }
  });


  menuItems.push({
    xtype: 'panel',
    layout: 'hbox',
    items: buttons
  });

  menu = new Ext.menu.Menu({
    layout: 'anchor',
    allowOtherMenus: true,
    items: menuItems
  });
  activeMenu = menu;
  var position = evt.getXY();
  position[0] -= (buttonWidth * 1.5) + 10; //horizontally center menu with the mouse
  menu.showAt(position);
  menu.get(0).focus(false, 50);
  menu.keyNav.disable();
  menu.on('hide', function () {
                    var graphMenuParams = Ext.getCmp('graphMenuParams');
                    if (graphMenuParams) {
                      graphMenuParams.destroy();
                    }
                  }
  );
  menu.on('destroy', function () {
                       optionsMenu.destroy();
                       operationsMenu.destroy();
                       functionsMenu.destroy();
                     }
  );
}


function removeUneditable (obj) {
  Ext.each(NOT_EDITABLE, function (p) {
    delete obj[p];
  });
  return obj;
}

function copyUneditable (src, dst) {
  Ext.each(NOT_EDITABLE, function (p) {
    if (src[p] === undefined) {
      delete dst[p];
    } else {
      dst[p] = src[p];
    }
  });
}

function breakoutGraph(record) {
  /* We have to gather some context from the
     graph target's expressions so we can reapply
     functions after the expressions get expanded. */
  var pathExpressions = [];
  var exprInfo = {};

  try {
    Ext.each(record.data.params.target, function(target) {
      var exprsInThisTarget = 0;
      map(target.split(','), function (arg) {
        var arglets = arg.split('(');
        map(arglets[arglets.length-1].split(')'), function (expr) {
          expr = expr.replace(/^\s*(.+?)\s*$/, '$1');
          if (expr.length == 0 || expr[0] == '"' || expr[0] == '\'') return;

          if (expr.match(/[a-z].*\..*[a-z]/i)) {
            exprsInThisTarget++;
            if (exprsInThisTarget > 1) {
              throw 'arrr!';
            }

            pathExpressions.push(expr);
            var i = target.indexOf(expr);
            exprInfo[expr] = {
              expr: expr,
              pre: target.substr(0, i),
              post: target.substr(i + expr.length)
            }

          }

        }); //map arglets
      }); //map args
    }); //each target
  } catch (err) {
    Ext.Msg.alert('Graph contains unbreakable target', 'Graph targets containing more than one metric expression cannot be broken out.');
    return;
  }

  Ext.Ajax.request({
    url: document.body.dataset.baseUrl + 'metrics/expand/',
    params: {
      groupByExpr: '1',
      leavesOnly: '1',
      query: pathExpressions
    },
    callback: function (options, success, response) {
                var responseObj = Ext.decode(response.responseText);
                graphStore.remove(record);
                for (var expr in responseObj.results) {
                  var pre = exprInfo[expr].pre;
                  var post = exprInfo[expr].post;
                  map(responseObj.results[expr], function (metricPath) {
                    metricPath = pre + metricPath + post;
                    graphAreaToggle(metricPath, {dontRemove: true, defaultParams: record.data.params});
                  });
                }
              }
  });
}

function mailGraph(record) {
  var mygraphParams = record.get('params');
  var newparams = Ext.encode(Ext.apply(mygraphParams, defaultGraphParams));

  var fromField = new Ext.form.TextField({
    fieldLabel: 'From',
    name: 'sender',
    width: 300,
    allowBlank: false
  });

  var toField = new Ext.form.TextField({
    fieldLabel: 'To',
    name: 'recipients',
    width: 300,
    allowBlank: false
  });

  var subjectField = new Ext.form.TextField({
    fieldLabel: 'Subject',
    name: 'subject',
    width: 300,
    allowBlank: false
  });

  var msgField = new Ext.form.TextArea({
    fieldLabel: 'Message',
    name: 'message',
    width: 300,
    height: 75
  });

  var graphParamsField = new Ext.form.TextField({
     name: 'graph_params',
     hidden: true,
     value: newparams
  });

  var contactForm = new Ext.form.FormPanel({
    width: 300,
    labelWidth: 90,
    items: [fromField, toField, subjectField, msgField, graphParamsField],
    buttons: [{
      text: 'Cancel',
      handler: function(){win.close();}
    }, {
         text: 'Send',
         handler: function(){
           if(contactForm.getForm().isValid()){
             contactForm.getForm().submit({
               url: document.body.dataset.baseUrl + 'dashboard/email',
               waitMsg: 'Processing Request',
               success: function (contactForm, response) {
                 win.close();
               }
             });
           }
         }
     }]
  });

  var win;

  win = new Ext.Window({
    title: 'Send graph via email',
    width: 450,
    height: 230,
    resizable: true,
    modal: true,
    layout: 'fit',
    items: [contactForm]
  });
  win.show();
}


function cloneGraph(record) {
  var index = graphStore.indexOf(record);
  var clone = cloneGraphRecord(record);
  graphStore.insert(index+1, [clone]);
  refreshGraphs();
}

function cloneGraphRecord(record) {
  //ensure we are working with copies, not references
  var props = {
    url: record.data.url,
    target: record.data.target,
    params: Ext.apply({}, record.data.params)
  };
  props.params.target = Ext.urlDecode(props.target).target;
  if (typeof props.params.target == 'string') {
    props.params.target = [props.params.target];
  }
  return new GraphRecord(props);
}

function historyGraph(record){

    var graphHistoryStore = new Ext.data.ArrayStore({
      fields: GraphRecord,
      listeners: {
        add: graphStoreUpdated,
        remove: graphStoreUpdated,
        update: graphStoreUpdated
      }
    });

    function getProps(record){
        var props = {
          url: record.data.url,
          target: record.data.target,
          params: Ext.apply({}, record.data.params)
        };
        props.params.target = Ext.urlDecode(props.target).target;
        if (typeof props.params.target == 'string') {
          props.params.target = [props.params.target];
        }

        props.params.width = '750';
        props.params.height = '300';
        props.params.until = '-';

        return props;
    }

    var props = getProps(record);
    var title = '';
    title = (props.params.title != undefined) ? props.params.title : '';

    props = getProps(record);
    props.params.title = title + ' 1 hour';
    props.params.from = '-1hour';
    graphHistoryStore.insert(0,new GraphRecord(props));

    props = getProps(record);
    props.params.title = title + ' 1 day';
    props.params.from = '-1day';
    graphHistoryStore.insert(1,new GraphRecord(props));

    props = getProps(record);
    props.params.title = title + ' 7 day';
    props.params.from = '-7day';
    graphHistoryStore.insert(2,new GraphRecord(props));

    props = getProps(record);
    props.params.title = title + ' 30 day';
    props.params.from = '-30day';
    graphHistoryStore.insert(3,new GraphRecord(props));

    var graphTemplate = new Ext.XTemplate(
      '<tpl for=".">',
        '<div class="graph-container">',
          '<div class="graph-overlay">',
            '<img class="graph-img" src="{url}" width="{width}" height="{height}" id="graph{index}">',
          '</div>',
        '</div>',
      '</tpl>',
      '<div class="x-clear"></div>'
    );

    updateDataHistory();

    function updateDataHistory(){
        graphHistoryStore.each(function (item, index) {
          var params = {};
          Ext.apply(params, defaultGraphParams);
          Ext.apply(params, item.data.params);
          //Ext.apply(params, GraphSize);
          params._uniq = Math.random();
          if (params.title === undefined && params.target.length == 1) {
            params.title = params.target[0];
          }

          if (!params.uniq === undefined) {
              delete params['uniq'];
          }
          item.set('url', '/render?' + Ext.urlEncode(params));
          item.set('width', item.data.params.width);
          item.set('height', item.data.params.height);
          item.set('index', index);
        });
    }

    var graphHistoryView = new Ext.DataView({
      store: graphHistoryStore,
      tpl: graphTemplate,
      overClass: 'graph-over',
      itemSelector: 'div.graph-container',
      emptyText: 'Configure your context above, and then select some metrics.',
      autoScroll: true,
      listeners: {
      }
    });

    var win = new Ext.Window({
      title: 'Graph History',
      width: 800,
      height: 800,
      resizable: true,
      modal: true,
      layout: 'fit',
      items: graphHistoryView
    });
    win.show();

}
function removeAllGraphs() {
  if (CONFIRM_REMOVE_ALL) {
    /*
    Ext.Msg.confirm(
      "Are you sure?",
      "Are you sure you want to remove all the graphs?",
      function (choice) {
        if (choice == 'yes') {
          graphStore.removeAll();
          refreshGraphs();
        }
      }
    );
    */
    var win;
    win = new Ext.Window({
      title: 'Remove All Graphs',
      width: 200,
      height: 120,
      modal: true,
      layout: 'vbox',
      layoutConfig: { align: 'center' },
      items: [
        {
          xtype: 'label',
          text: 'Are You Sure?',
          style: 'font-size: large;'
        }, {
          id: 'always-ask-me',
          xtype: 'checkbox',
          boxLabel: 'Always Ask Me',
          name: 'ask-me',
          inputValue: 'yes',
          checked: true
        }
      ],
      buttonAlign: 'center',
      buttons: [
        {
          text: 'Yes',
          handler: function () {
                     if (Ext.getCmp('always-ask-me').getValue()) {
                       CONFIRM_REMOVE_ALL = true;
                       cookieProvider.set('confirm-remove-all', 'true');
                     } else {
                       CONFIRM_REMOVE_ALL = false;
                       cookieProvider.set('confirm-remove-all', 'false');
                     }
                     graphStore.removeAll();
                     refreshGraphs();
                     win.close();
                   }
        }, {
          text: 'No',
          handler: function () { win.close(); }
        }
      ]
    });
    win.show();
  } else {
    graphStore.removeAll();
    refreshGraphs();
  }
}


function toggleToolbar() {
  var tbar = graphArea.getTopToolbar();
  tbar.setVisible( ! tbar.isVisible() );
  graphArea.doLayout();
}

function toggleNavBar() {
  navBar.toggleCollapse(true);
}

function focusCompleter() {
  if (metricSelectorTextField) metricSelectorTextField.focus(false, 50);
}

/* Keyboard shortcuts */
var keyEventHandlers = {
  toggle_toolbar: toggleToolbar,
  toggle_metrics_panel: toggleNavBar,
  give_completer_focus: focusCompleter,
  erase_all_graphs: function () {
      graphStore.removeAll();
      refreshGraphs();
      graphStoreUpdated();
    },
  completer_add_metrics: function () {
      if (metricSelectorGrid) {
        metricSelectorGrid.getStore().each(function (record) {
          if (record.data.path[ record.data.path.length - 1] != '.') {
            graphAreaToggle(record.data.path, {dontRemove: true});
          }
        });
        focusCompleter();
      }
    },
  completer_del_metrics: function () {
      if (metricSelectorGrid) {
        metricSelectorGrid.getStore().each(function (record) {
          graphAreaToggle(record.data.path, {onlyRemove: true});
        });
        focusCompleter();
      }
    },
  save_dashboard: function () {
      if (dashboardName == null) {
        saveDashboard();
      } else {
        sendSaveRequest(dashboardName);
      }
    }
};

var specialKeys = {
  space: 32,
  enter: Ext.EventObject.ENTER,
  backspace: Ext.EventObject.BACKSPACE
};

var keyMapConfigs = [];

for (var event_name in UI_CONFIG.keyboard_shortcuts) {
  var config = {handler: keyEventHandlers[event_name]};
  if (!config.handler) {
    continue;
  }
  var keyString = UI_CONFIG.keyboard_shortcuts[event_name];
  var keys = keyString.split('-');
  config.ctrl = keys.indexOf('ctrl') > -1;
  config.alt = keys.indexOf('alt') > -1;
  config.shift = keys.indexOf('shift') > -1;
  config.key = keys[keys.length - 1];
  if (specialKeys[config.key]) {
    config.key = specialKeys[config.key];
  }
  keyMapConfigs.push(config);
}

var keyMap = new Ext.KeyMap(document, keyMapConfigs);


/* Dashboard functions */
function editDashboard() {
  var editDashboardWin = new Ext.Window({
    title: 'Edit Dashboard',
    id: 'editor-window',
    width: 700,
    height: 500,
    layout: 'vbox',
    layoutConfig: {align: 'stretch', pack: 'start'},
    modal: true,
    items: [
      {
        xtype: 'container',
        flex: 1,
        id: 'editor',
        title: 'ace',
        listeners: { resize: function () { if (editor) editor.resize(); } }
      }
    ],
    listeners: {
      afterrender: {
        scope: this,
        fn: function (obj) { setupEditor(obj.body.dom); getInitialState() }
      }
    },
    buttons: [
      {text: 'Update (doesn\'t save)', handler: updateAfterEdit},
      {text: 'Cancel', handler: function () { editDashboardWin.close(); } }
    ]
  });
  function updateAfterEdit(btn, target) {
    var graphString = editor.getSession().getValue();
    var targets = JSON.parse(graphString);
    graphStore.removeAll();
    var graphs = [];
    for (var i = 0; i < targets.length; i++) {
      var myParams = {};
      Ext.apply(myParams, targets[i]);
      var urlParams = {};
      Ext.apply(urlParams, defaultGraphParams);
      Ext.apply(urlParams, GraphSize);
      Ext.apply(urlParams, myParams);
      graphs.push([
        Ext.urlEncode({target: targets[i].target}),
        myParams,
        document.body.dataset.baseUrl + 'render?' + Ext.urlEncode(urlParams)
      ]);
    }
    graphStore.loadData(graphs);
    refreshGraphs();
    editDashboardWin.close();
  }
  function getInitialState() {
    var graphs = [];
    graphStore.each(function () {
      var params = {};
      Ext.apply(params, this.data.params);
      delete params['from'];
      delete params['until'];
      graphs.push(params);
    });
    editor.getSession().setValue(JSON.stringify(graphs, null, 2));
  }
  function setupEditor(obj) {
    editor = ace.edit('editor');
    editor.setTheme('ace/theme/textmate');
    var JSONMode = require('ace/mode/json').Mode;
    var session = editor.getSession();
    session.setMode(new JSONMode());
    session.setUseSoftTabs(true);
    session.setTabSize(2);
  }
  editDashboardWin.show();
}

function saveDashboard() {
  Ext.Msg.prompt(
    'Save Dashboard',
    'Enter the name to save this dashboard as',
    function (button, text) {
      if (button == 'ok') {
        setDashboardName(text);
        sendSaveRequest(text);
      }
    },
    this,
    false,
    (dashboardName) ? dashboardName : ''
  );
}

function saveTemplate() {
  var nameField = new Ext.form.TextField({
    id: 'dashboard-save-template-name',
    fieldLabel: 'Template Name',
    width: 240,
    allowBlank: false,
    align: 'center',
    value: dashboardName ? dashboardName.split('/')[0]: '',
  });

  var keyField = new Ext.form.TextField({
    id: 'dashboard-save-template-key',
    fieldLabel: 'String to replace',
    width: 240,
    allowBlank: false,
    align: 'center',
  });

  var win;

  function save() {
    sendSaveTemplateRequest(nameField.getValue(), keyField.getValue());
    win.close();
  }

  win = new Ext.Window({
    title: 'Save dashboard as a template',
    width: 400,
    height: 120,
    resizable: false,
    layout: 'form',
    labelAlign: 'right',
    labelWidth: 120,
    items: [nameField,keyField],
    buttonAlign: 'center',
    buttons: [
      {text: 'Ok', handler: save},
      {text: 'Cancel', handler: function () { win.close(); } }
    ]
  });
  win.show();
}

function sendSaveTemplateRequest(name, key) {
  Ext.Ajax.request({
    url: document.body.dataset.baseUrl + 'dashboard/save_template/' + name + '/' + key,
    method: 'POST',
    params: {
      state: Ext.encode( getState() )
    },
    success: function (response) {
               var result = Ext.decode(response.responseText);
               if (result.error) {
                 Ext.Msg.alert('Error', htmlEncode('There was an error saving this dashboard as a template: ' + result.error));
               }
             },
    failure: failedAjaxCall
  });
}

function sendSaveRequest(name) {
  Ext.Ajax.request({
    url: document.body.dataset.baseUrl + 'dashboard/save/' + name,
    method: 'POST',
    params: {
      state: Ext.encode( getState() )
    },
    success: function (response) {
               var result = Ext.decode(response.responseText);
               if (result.error) {
                 Ext.Msg.alert('Error', htmlEncode('There was an error saving this dashboard: ' + result.error));
               }
               if(newURL) {
                 window.location = newURL;
               } else {
                 changeHash(name);
               }
             },
    failure: failedAjaxCall
  });
}

function sendLoadRequest(name) {
  Ext.Ajax.request({
    url: document.body.dataset.baseUrl + 'dashboard/load/' + name,
    success: function (response) {
               var result = Ext.decode(response.responseText);
               if (result.error) {
                 Ext.Msg.alert('Error Loading Dashboard', htmlEncode(result.error));
               } else {
                 applyState(result.state);
                 navBar.collapse(false);
               }
             },
    failure: failedAjaxCall
  });
}

function sendLoadTemplateRequest(name, value) {
  var urlparts = window.location.href.split('#')
  if(urlparts[0].split('?')[1]) {
    var newLocation = urlparts[0].split('?')[0] + '#'+name+'/'+value;
    window.location.href = newLocation;
  } else {
    Ext.Ajax.request({
      url: document.body.dataset.baseUrl + 'dashboard/load_template/' + name + '/' + value,
      success: function (response) {
               var result = Ext.decode(response.responseText);
               if (result.error) {
                 Ext.Msg.alert('Error Loading Template', htmlEncode(result.error));
               } else {
                 applyState(result.state);
                 navBar.collapse(false);
               }
             },
      failure: failedAjaxCall
    });
  }
}

function getState() {
  var graphs = [];
  graphStore.each(
    function (record) {
      graphs.push([
        record.data.id,
        record.data.target,
        record.data.params,
        record.data.url
      ]);
    }
  );

  return {
    name: dashboardName,
    timeConfig: TimeRange,
    refreshConfig: {
      enabled: Ext.getCmp('auto-refresh-button').pressed,
      interval: refreshTask.interval
    },
    graphSize: GraphSize,
    defaultGraphParams: defaultGraphParams,
    graphs: graphs
  };
}

function applyState(state) {
  setDashboardName(state.name);

  //state.timeConfig = {type, quantity, units, untilQuantity, untilUnits, startDate, startTime, endDate, endTime}
  var timeConfig = state.timeConfig
  TimeRange.type = timeConfig.type;
  TimeRange.relativeStartQuantity = timeConfig.relativeStartQuantity;
  TimeRange.relativeStartUnits = timeConfig.relativeStartUnits;
  TimeRange.relativeUntilQuantity = timeConfig.relativeUntilQuantity;
  TimeRange.relativeUntilUnits = timeConfig.relativeUntilUnits;
  TimeRange.startDate = new Date(timeConfig.startDate);
  TimeRange.startTime = timeConfig.startTime;
  TimeRange.endDate = new Date(timeConfig.endDate);
  TimeRange.endTime = timeConfig.endTime;

  if (queryString.from && queryString.until) {
    // The URL contains a "from" and "until" parameters (format "YYYY-MM-DDThh:mm:ss") => use the timestamps as default absolute range of the dashboard
    var from = new Date(queryString.from);
    var until = new Date(queryString.until);

    TimeRange.startDate = from;
    TimeRange.startTime = from.format('H:m');
    TimeRange.endDate = until;
    TimeRange.endTime = until.format('H:m');
    TimeRange.type = 'absolute';

    state.timeConfig = TimeRange;

    state.defaultGraphParams.from = from.format('H:i_Ymd');
    state.defaultGraphParams.until = until.format('H:i_Ymd');
  }

  updateTimeText();



  //state.refreshConfig = {enabled, interval}
  var refreshConfig = state.refreshConfig;
  if (refreshConfig.enabled) {
    stopTask(refreshTask);
    startTask(refreshTask);
    Ext.getCmp('auto-refresh-button').toggle(true);
  } else {
    stopTask(refreshTask);
    Ext.getCmp('auto-refresh-button').toggle(false);
  }
  //refreshTask.interval = refreshConfig.interval;
  updateAutoRefresh(refreshConfig.interval / 1000);

  //state.graphSize = {width, height}
  var graphSize = state.graphSize;
  GraphSize.width = graphSize.width;
  GraphSize.height = graphSize.height;

  //state.defaultGraphParams = {...}
  defaultGraphParams = state.defaultGraphParams || originalDefaultGraphParams;

  //state.graphs = [ [id, target, params, url], ... ]
  //Fix url param to be correct for this document.body.dataset.baseUrl
  var graphs = [];
  for (var i = 0; i < state.graphs.length; i++) {
    var myParams = {};
    var renderType = state.graphs[i][3];
    Ext.apply(myParams, state.graphs[i][1]);
    var urlParams = {};
    Ext.apply(urlParams, defaultGraphParams);
    Ext.apply(urlParams, GraphSize);
    Ext.apply(urlParams, myParams);
    graphs.push([
      state.graphs[i][0],
      myParams,
      document.body.dataset.baseUrl + 'render?' + Ext.urlEncode(urlParams),
      renderType
    ]);
  }
  graphStore.loadData(graphs);

  refreshGraphs();
}

function deleteDashboard(name) {
  Ext.Ajax.request({
    url: document.body.dataset.baseUrl + 'dashboard/delete/' + name,
    success: function (response) {
      var result = Ext.decode(response.responseText);
      if (result.error) {
        Ext.Msg.alert('Error', htmlEncode('Failed to delete dashboard \'' + name + '\': ' + result.error));
      } else {
        Ext.Msg.alert('Dashboard Deleted', htmlEncode('The ' + name + ' dashboard was deleted successfully.'));
      }
    },
    failure: failedAjaxCall
  });
}

function deleteTemplate(name) {
  Ext.Ajax.request({
    url: document.body.dataset.baseUrl + 'dashboard/delete_template/' + name,
    success: function (response) {
      var result = Ext.decode(response.responseText);
      if (result.error) {
        Ext.Msg.alert('Error', htmlEncode('Failed to delete template \'' + name + '\': ' + result.error));
      } else {
        Ext.Msg.alert('Template Deleted', htmlEncode('The ' + name + ' template was deleted successfully.'));
      }
    },
    failure: failedAjaxCall
  });
}

function setDashboardName(name) {
  dashboardName = name;
  var saveButton = Ext.getCmp('dashboard-save-button');

  if (name == null || !hasPermission('change')) {
    dashboardURL = null;
    document.title = 'untitled - Graphite Dashboard';
    navBar.setTitle('untitled');
    saveButton.setText('Save');
    saveButton.disable();
  } else {
    var urlparts = location.href.split('#')[0].split('/');
    var i = urlparts.indexOf('dashboard');
    if (i == -1) {
      Ext.Msg.alert('Error', htmlEncode('urlparts = ' + Ext.encode(urlparts) + ' and indexOf(dashboard) = ' + i));
      return;
    }
    urlparts = urlparts.slice(0, i+1);
    urlparts.push( encodeURI(name) )
    dashboardURL = urlparts.join('/');

    document.title = name + ' - Graphite Dashboard';
    changeHash(name);
    navBar.setTitle(name + ' - (' + dashboardURL + ')');
    saveButton.setText('Save "' + name + '"');
    saveButton.enable();
  }
}

function failedAjaxCall(response, options) {
  Ext.Msg.alert(
    'Ajax Error',
    htmlEncode('Ajax call failed, response was :' + response.responseText)
  );
}

var configureUIWin;
function configureUI() {

  if (configureUIWin) {
    configureUIWin.close();
  }

  function updateOrientation() {
    if (Ext.getCmp('navbar-left-radio').getValue()) {
      updateNavBar('west');
    } else {
      updateNavBar('north');
    }
    configureUIWin.close();
    configureUIWin = null;
  }

  configureUIWin = new Ext.Window({
    title: 'Configure UI',
    layout: 'form',
    width: 300,
    height: 125,
    labelWidth: 120,
    labelAlign: 'right',
    items: [
      {
        id: 'navbar-left-radio',
        xtype: 'radio',
        fieldLabel: 'Navigation Mode',
        boxLabel: 'Tree (left nav)',
        name: 'navbar-position',
        inputValue: 'left',
        checked: (NAV_BAR_REGION == 'west')
      }, {
        id: 'navbar-top-radio',
        xtype: 'radio',
        fieldLabel: '',
        boxLabel: 'Completer (top nav)',
        name: 'navbar-position',
        inputValue: 'top',
        checked: (NAV_BAR_REGION == 'north')
      }
    ],
    buttons: [
      {text: 'Ok', handler: updateOrientation},
      {text: 'Cancel', handler: function () { configureUIWin.close(); configureUIWin = null; } }
    ]
  });
  configureUIWin.show();
}

function updateNavBar(region) {
  if (region == NAV_BAR_REGION) {
    return;
  }

  cookieProvider.set('navbar-region', region);
  NAV_BAR_REGION = region;

  if (graphStore.getCount() == 0) {
    window.location.reload()
  } else {
    Ext.Msg.alert('Cookie Updated', 'You must refresh the page to update the nav bar\'s location.');
    //TODO prompt the user to save their dashboard and refresh for them
  }
}

// Dashboard Finder
function showDashboardFinder() {
  var win;
  var dashboardsList;
  var queryField;
  var dashboardsStore = new Ext.data.JsonStore({
    url: document.body.dataset.baseUrl + 'dashboard/find/',
    method: 'GET',
    params: {query: 'e'},
    fields: [{
      name: 'name',
      sortType: function(value) {
        // Make sorting case-insensitive
        return value.toLowerCase();
      }
    }],
    root: 'dashboards',
    sortInfo: {
      field: 'name',
      direction: 'ASC'
    },
    listeners: {
      beforeload: function (store) {
                    store.setBaseParam('query', queryField.getValue());
      }
    }
  });

  function openSelected() {
    var selected = dashboardsList.getSelectedRecords();
    if (selected.length > 0) {
      sendLoadRequest(selected[0].data.name);
    }
    win.close();
  }

  function deleteSelected() {
    var selected = dashboardsList.getSelectedRecords();
    if (selected.length > 0) {
      var record = selected[0];
      var name = record.data.name;

      Ext.Msg.confirm(
       'Delete Dashboard',
        htmlEncode('Are you sure you want to delete the ' + name + ' dashboard?'),
        function (button) {
          if (button == 'yes') {
            deleteDashboard(name);
            dashboardsStore.remove(record);
            dashboardsList.refresh();
          }
        }
      );
    }
  }

  dashboardsList = new Ext.list.ListView({
    columns: [
      {header: 'Dashboard', width: 1.0, dataIndex: 'name', sortable: false}
    ],
    columnSort: false,
    emptyText: 'No dashboards found',
    hideHeaders: true,
    listeners: {
      selectionchange: function (listView, selections) {
                         if (listView.getSelectedRecords().length == 0) {
                           Ext.getCmp('finder-open-button').disable();
                           Ext.getCmp('finder-delete-button').disable();
                         } else {
                           Ext.getCmp('finder-open-button').enable();
                           if (hasPermission('delete')) {
                             Ext.getCmp('finder-delete-button').enable();
                           } else {
                             Ext.getCmp('finder-delete-button').disable();
                           }
                         }
                       },

      dblclick: function (listView, index, node, e) {
                  var record = dashboardsStore.getAt(index);
                  sendLoadRequest(record.data.name);
                  win.close();
                }
    },
    overClass: '',
    region: 'center',
    reserveScrollOffset: true,
    singleSelect: true,
    store: dashboardsStore,
    style: 'background-color: white;'
  });

  var lastQuery = null;
  var queryUpdateTask = new Ext.util.DelayedTask(
    function () {
      var currentQuery = queryField.getValue();
      if (lastQuery != currentQuery) {
        dashboardsStore.load();
      }
      lastQuery = currentQuery;
    }
  );

  queryField = new Ext.form.TextField({
    region: 'south',
    emptyText: 'filter dashboard listing',
    enableKeyEvents: true,
    listeners: {
      keyup: function (field, e) {
                  if (e.getKey() == e.ENTER) {
                    sendLoadRequest(field.getValue());
                    win.close();
                  } else {
                    queryUpdateTask.delay(FINDER_QUERY_DELAY);
                  }
                }
    }
  });

  win = new Ext.Window({
    title: 'Dashboard Finder',
    width: 400,
    height: 500,
    layout: 'border',
    modal: true,
    items: [
      dashboardsList,
      queryField
    ],
    buttons: [
      {
        id: 'finder-open-button',
        text: 'Open',
        disabled: true,
        handler: openSelected
      }, {
        id: 'finder-delete-button',
        text: 'Delete',
        disabled: true,
        handler: deleteSelected
      }, {
        text: 'Close',
        handler: function () { win.close(); }
      }
    ]
  });
  dashboardsStore.load();
  win.show();
}

// Template Finder
function showTemplateFinder() {
  var win;
  var templatesList;
  var queryField;
  var valueField;
  var templatesStore = new Ext.data.JsonStore({
    url: document.body.dataset.baseUrl + 'dashboard/find_template/',
    method: 'GET',
    params: {query: 'e'},
    fields: ['name'],
    root: 'templates',
    listeners: {
      beforeload: function (store) {
                    store.setBaseParam('query', queryField.getValue());
                  }
    }
  });

  function openSelected() {
    var selected = templatesList.getSelectedRecords();
    if (selected.length > 0) {
      sendLoadTemplateRequest(selected[0].data.name, valueField.getValue());
    }
    win.close();
  }

  function deleteSelected() {
    var selected = templatesList.getSelectedRecords();
    if (selected.length > 0) {
      var record = selected[0];
      var name = record.data.name;

      Ext.Msg.confirm(
        'Delete Template',
        htmlEncode('Are you sure you want to delete the ' + name + ' template?'),
        function (button) {
          if (button == 'yes') {
            deleteTemplate(name);
            templatesStore.remove(record);
            templatesList.refresh();
          }
        }
      );
    }
  }

  templatesList = new Ext.list.ListView({
    columns: [
      {header: 'Template', width: 1.0, dataIndex: 'name', sortable: false}
    ],
    columnSort: false,
    emptyText: 'No templates found',
    hideHeaders: true,
    listeners: {
      selectionchange: function (listView, selections) {
                         if (listView.getSelectedRecords().length == 0) {
                           Ext.getCmp('finder-open-button').disable();
                           Ext.getCmp('finder-delete-button').disable();
                         } else {
                           if (valueField.getValue()) {
                             Ext.getCmp('finder-open-button').enable();
                           }
                           Ext.getCmp('finder-delete-button').enable();
                         }
                       },

    },
    overClass: '',
    region: 'center',
    reserveScrollOffset: true,
    singleSelect: true,
    store: templatesStore,
    style: 'background-color: white;'
  });

  var lastQuery = null;
  var queryUpdateTask = new Ext.util.DelayedTask(
    function () {
      var currentQuery = queryField.getValue();
      if (lastQuery != currentQuery) {
        templatesStore.load();
      }
      lastQuery = currentQuery;
    }
  );

  queryField = new Ext.form.TextField({
    region: 'south',
    emptyText: 'filter template listing',
    enableKeyEvents: true,
    listeners: {
      keyup: function (field, e) {
                  if (e.getKey() == e.ENTER) {
                    sendLoadRequest(field.getValue(), reset_params=true);
                    win.close();
                  } else {
                    queryUpdateTask.delay(FINDER_QUERY_DELAY);
                  }
                }
    }
  });

  valueField = new Ext.form.TextField({
    region: 'north',
    emptyText: 'Value to use'
  });

  win = new Ext.Window({
    title: 'Template Finder',
    width: 400,
    height: 500,
    layout: 'border',
    modal: true,
    items: [
      valueField,
      templatesList,
      queryField,
    ],
    buttons: [
      {
        id: 'finder-open-button',
        text: 'Open',
        disabled: true,
        handler: openSelected
      }, {
        id: 'finder-delete-button',
        text: 'Delete',
        disabled: true,
        handler: deleteSelected
      }, {
        text: 'Close',
        handler: function () { win.close(); }
      }
    ]
  });
  templatesStore.load();
  win.show();
}

/* Graph Options API (to reuse createOptionsMenu from composer_widgets.js) */
function updateGraph() {
  refreshGraphs();
  var graphMenuParams = Ext.getCmp('graphMenuParams');
  if (graphMenuParams) {
    var editParams = Ext.apply({}, selectedRecord.data.params);
    removeUneditable(editParams);
    graphMenuParams.setValue( Ext.urlEncode(editParams) );
  }
}

function getParam(param) {
  return selectedRecord.data.params[param];
}

function setParam(param, value) {
  selectedRecord.data.params[param] = value;
  selectedRecord.commit();
}

function removeParam(param) {
  delete selectedRecord.data.params[param];
  selectedRecord.commit();
}


function removeTargetFromSelectedGraph(target) {
  selectedRecord.data.params.target.remove(target);
  selectedRecord.data.target = Ext.urlEncode({target: selectedRecord.data.params.target});
}

function getSelectedTargets() {
  if (targetGrid) {
    return map(targetGrid.getSelectionModel().getSelections(), function (r) {
      return r.data.target;
    });
  }
  return [];
}

function applyFuncToEach(funcName, extraArg) {

  function applyFunc() {
    Ext.each(targetGrid.getSelectionModel().getSelections(),
      function (record) {
        var target = record.data.target;
        var newTarget;
        var targetStore = targetGrid.getStore();

        targetStore.remove(record);
        removeTargetFromSelectedGraph(target);

        if (extraArg) {
          newTarget = funcName + '(' + target + ',' + extraArg + ')';
        } else {
          newTarget = funcName + '(' + target + ')';
        }

        // Add newTarget to selectedRecord
        targetStore.add([ new targetStore.recordType({target: newTarget}, newTarget) ]);
        targetGrid.getSelectionModel().selectRow(targetStore.findExact('target', newTarget), true);
      }
    );
    refreshGraphs();
  }
  return applyFunc;
}

function applyFuncToEachWithInput (funcName, question, options) {
  if (options == null) {
    options = {};
  }

 function applyFunc() {
    Ext.MessageBox.prompt(
      'Input Required', //title
      question, //message
      function (button, inputValue) { //handler
        if (button == 'ok' && (options.allowBlank || inputValue != '')) {
          if (options.quote) {
            inputValue = '"' + inputValue + '"';
          }
          applyFuncToEach(funcName, inputValue)();
        }
      },
      this, //scope
      false, //multiline
      '' //initial value
    );
  }
  applyFunc = applyFunc.createDelegate(this);
  return applyFunc;
}

function applyFuncToAll (funcName) {
  function applyFunc() {
    var args = getSelectedTargets().join(',');
    var newTarget = funcName + '(' + args + ')';
    var targetStore = targetGrid.getStore();

    Ext.each(targetGrid.getSelectionModel().getSelections(),
      function (record) {
        targetStore.remove(record);
        removeTargetFromSelectedGraph(record.data.target);
      }
    );
    targetStore.add([ new targetStore.recordType({target: newTarget}, newTarget) ]);
    targetGrid.getSelectionModel().selectRow(targetStore.findExact('target', newTarget), true);
    refreshGraphs();
  }
  applyFunc = applyFunc.createDelegate(this);
  return applyFunc;
}

function removeOuterCall() { // blatantly repurposed from composer_widgets.js (don't hate)
  Ext.each(targetGrid.getSelectionModel().getSelections(), function (record) {
    var target = record.data.target;
    var targetStore = targetGrid.getStore();
    var args = [];
    var i, c;
    var lastArg = 0;
    var depth = 0;
    var argString = target.replace(/^[^(]+\((.+)\)/, '$1'); //First we strip it down to just args

    for (i = 0; i < argString.length; i++) {
      switch (argString.charAt(i)) {
        case '(': depth += 1; break;
        case '{': depth += 1; break;
        case ')': depth -= 1; break;
        case '}': depth -= 1; break;
        case ',':
          if (depth > 0) { continue; }
          if (depth < 0) { Ext.Msg.alert('Malformed target, cannot remove outer call.'); return; }
          args.push( argString.substring(lastArg, i).replace(/^\s+/, '').replace(/\s+$/, '') );
          lastArg = i + 1;
          break;
      }
    }
    args.push( argString.substring(lastArg, i) );

    targetStore.remove(record);
    selectedRecord.data.params.target.remove(target);

    Ext.each(args, function (arg) {
      if (!arg.match(/^([0123456789\.]+|".+")$/)) { //Skip string and number literals
        targetStore.add([ new targetStore.recordType({target: arg}) ]);
        targetGrid.getSelectionModel().selectRow(targetStore.findExact('target', arg), true);
      }
    });
  });
  refreshGraphs();
}

function saveDefaultGraphParams() {
  cookieProvider.set('defaultGraphParams', Ext.encode(defaultGraphParams));
}


/* Cookie stuff */
function getContextFieldCookie(field) {
  return cookieProvider.get(field);
}

function setContextFieldCookie(field, value) {
  cookieProvider.set(field, value);
}

/* Misc */
function uniq(myArray) {
  var uniqArray = [];
  for (var i=0; i<myArray.length; i++) {
    if (uniqArray.indexOf(myArray[i]) == -1) {
      uniqArray.push(myArray[i]);
    }
  }
  return uniqArray;
}

function map(myArray, myFunc) {
  var results = [];
  for (var i=0; i<myArray.length; i++) {
    results.push( myFunc(myArray[i]) );
  }
  return results;
}

function getLoginMenuItemText() {
  if (isLoggedIn()) {
    return 'Log Out From "' + userName + '"';
  } else {
    return 'Log In';
  }
}

/* After login/logout, make any necessary adjustments to Dashboard menu items (text and/or disabled) */
function postLoginMenuAdjust() {
  Ext.getCmp('dashboard-login-button').setText(getLoginMenuItemText());
  Ext.getCmp('dashboard-save-button').setDisabled(dashboardName == null || !hasPermission('change'));
  Ext.getCmp('dashboard-save-as-button').setDisabled(!hasPermission('change'));
}

function showLoginForm() {
  var login = new Ext.FormPanel({
    labelWidth: 80,
    frame: true,
    title: 'Please Login',
    defaultType: 'textfield',
    monitorValid: true,

    items: [{
        fieldLabel: 'Username',
        name: 'username',
        allowBlank: false,
        listeners: {
          afterrender: function(field) { field.focus(false, 100); }
        }
      },{
        fieldLabel: 'Password',
        name: 'password',
        inputType: 'password',
        allowBlank: false
      }
    ],
    buttons: [
      {text: 'Login', formBind: true, handler: doLogin},
      {text: 'Cancel', handler: function () { win.close(); } }
    ]
  });

  function doLogin() {
    login.getForm().submit({
      method: 'POST',
      url: document.body.dataset.baseUrl + 'dashboard/login',
      waitMsg: 'Authenticating...',
      success: function(form, action) {
        userName = form.findField('username').getValue();
        permissions = action.result.permissions;
        postLoginMenuAdjust();
        win.close();
      },
      failure: function(form, action) {
        if (action.failureType == 'server') {
          var obj = Ext.util.JSON.decode(action.response.responseText);
          Ext.Msg.alert('Login Failed!', htmlEncode(obj.errors.reason));
        } else {
          Ext.Msg.alert('Warning!', htmlEncode('Authentication server is unreachable : ' + action.response.responseText));
        }
        login.getForm().reset();
      }
    });
  }

  var win = new Ext.Window({
    layout: 'fit',
    width: 300,
    height: 150,
    closable: false,
    resizable: false,
    plain: true,
    border: false,
    items: [login]
  });
  win.show();
}

function logout() {
  Ext.Ajax.request({
    url: document.body.dataset.baseUrl + 'dashboard/logout',
    method: 'POST',
    success: function() {
      userName = null;
      permissions = permissionsUnauthenticated;
      postLoginMenuAdjust();
    },
    failure: function() {
      // Probably because they no longer have a valid session - assume they're now logged out
      userName = null;
      permissions = permissionsUnauthenticated;
      postLoginMenuAdjust();
    }
  });
}
