text: // Copyright (C) 2013 Google Inc. All rights reserved.
  //
  // Redistribution and use in source and binary forms, with or without
  // modification, are permitted provided that the following conditions are
  // met:
  //
  //         * Redistributions of source code must retain the above copyright
  // notice, this list of conditions and the following disclaimer.
  //         * Redistributions in binary form must reproduce the above
  // copyright notice, this list of conditions and the following disclaimer
  // in the documentation and/or other materials provided with the
  // distribution.
  //         * Neither the name of Google Inc. nor the names of its
  // contributors may be used to endorse or promote products derived from
  // this software without specific prior written permission.
  //
  // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
  // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
  // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
  // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
  // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
  // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
  // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
  // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
  // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
  // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  
  
  var history = history || {};
  
  (function() {
  
  history.DEFAULT_CROSS_DASHBOARD_STATE_VALUES = {
      showAllRuns: false,
      testType: 'browser_tests',
      useTestData: false,
  }
  
  history.validateParameter = function(state, key, value, validateFn)
  {
      if (validateFn()) {
          state[key] = value;
          return true;
      } else {
          console.log(key + ' value is not valid: ' + value);
          return false;
      }
  }
  
  // TODO(jparent): Make private once callers move here.
  history.queryHashAsMap = function()
  {
      var hash = window.location.hash;
      var paramsList = hash ? hash.substring(1).split('&') : [];
      var paramsMap = {};
      var invalidKeys = [];
      for (var i = 0; i < paramsList.length; i++) {
          var thisParam = paramsList[i].split('=');
          if (thisParam.length != 2) {
              console.log('Invalid query parameter: ' + paramsList[i]);
              continue;
          }
  
          paramsMap[thisParam[0]] = decodeURIComponent(thisParam[1]);
      }
  
      return paramsMap;
  }
  
  history._diffStates = function(oldState, newState)
  {
      // If there is no old state, everything in the current state is new.
      if (!oldState)
          return newState;
  
      var changedParams = {};
      for (curKey in newState) {
          var oldVal = oldState[curKey];
          var newVal = newState[curKey];
          // Add new keys or changed values.
          if (!oldVal || oldVal != newVal)
              changedParams[curKey] = newVal;
      }
      return changedParams;
  }
  
  history._fillMissingValues = function(to, from)
  {
      for (var state in from) {
          if (!(state in to))
              to[state] = from[state];
      }
  }
  
  history.History = function(configuration)
  {
      this.crossDashboardState = {};
      this.dashboardSpecificState = {};
  
      if (configuration) {
          this._defaultDashboardSpecificStateValues = configuration.defaultStateValues;
          this._handleValidHashParameter = configuration.handleValidHashParameter;
          this._handleQueryParameterChange = configuration.handleQueryParameterChange || function(historyInstance, params) { return true; };
          this._dashboardSpecificInvalidatingParameters = configuration.invalidatingHashParameters;
          this._generatePage = configuration.generatePage;
      }
  }
  
  history.reloadRequiringParameters = ['showAllRuns', 'testType'];
  
  history.History.prototype = {
      initialize: function()
      {
          window.onhashchange = this._handleLocationChange.bind(this);
          this._handleLocationChange();
      },
      isLayoutTestResults: function()
      {
          // TODO(crbug/706192): remove webkit_tests once this step name no longer exists
          return this.crossDashboardState.testType == 'webkit_tests' ||
              this.crossDashboardState.testType.startsWith('webkit_layout_tests');
      },
      isGPUTestResults: function()
      {
          return this.crossDashboardState.testType == 'gpu_tests';
      },
      parseCrossDashboardParameters: function()
      {
          this.crossDashboardState = {};
          var parameters = history.queryHashAsMap();
          for (parameterName in history.DEFAULT_CROSS_DASHBOARD_STATE_VALUES)
              this.parseParameter(parameters, parameterName);
  
          history._fillMissingValues(this.crossDashboardState, history.DEFAULT_CROSS_DASHBOARD_STATE_VALUES);
      },
      _parseDashboardSpecificParameters: function()
      {
          this.dashboardSpecificState = {};
          var parameters = history.queryHashAsMap();
          for (parameterName in this._defaultDashboardSpecificStateValues)
              this.parseParameter(parameters, parameterName);
      },
      // TODO(jparent): Make private once callers move here.
      parseParameters: function()
      {
          var oldCrossDashboardState = this.crossDashboardState;
          var oldDashboardSpecificState = this.dashboardSpecificState;
  
          this.parseCrossDashboardParameters();
  
          // Some parameters require loading different JSON files when the value changes. Do a reload.
          if (Object.keys(oldCrossDashboardState).length) {
              for (var key in this.crossDashboardState) {
                  if (oldCrossDashboardState[key] != this.crossDashboardState[key] && history.reloadRequiringParameters.indexOf(key) != -1) {
                      window.location.reload();
                      return false;
                  }
              }
          }
  
          this._parseDashboardSpecificParameters();
          var dashboardSpecificDiffState = history._diffStates(oldDashboardSpecificState, this.dashboardSpecificState);
  
          history._fillMissingValues(this.dashboardSpecificState, this._defaultDashboardSpecificStateValues);
  
          // FIXME: dashboard_base shouldn't know anything about specific dashboard specific keys.
          if (dashboardSpecificDiffState.builder)
              delete this.dashboardSpecificState.tests;
          if (this.dashboardSpecificState.tests)
              delete this.dashboardSpecificState.builder;
  
          var shouldGeneratePage = true;
          if (Object.keys(dashboardSpecificDiffState).length)
              shouldGeneratePage = this._handleQueryParameterChange(this, dashboardSpecificDiffState);
          return shouldGeneratePage;
      },
      // TODO(jparent): Make private once callers move here.
      parseParameter: function(parameters, key)
      {
          if (!(key in parameters))
              return;
          var value = parameters[key];
          if (!this._handleValidHashParameterWrapper(key, value))
              console.log("Invalid query parameter: " + key + '=' + value);
      },
      // Takes a key and a value and sets the this.dashboardSpecificState[key] = value iff key is
      // a valid hash parameter and the value is a valid value for that key. Handles
      // cross-dashboard parameters then falls back to calling
      // handleValidHashParameter for dashboard-specific parameters.
      //
      // @return {boolean} Whether the key what inserted into the this.dashboardSpecificState.
      _handleValidHashParameterWrapper: function(key, value)
      {
          switch(key) {
          case 'testType':
              history.validateParameter(this.crossDashboardState, key, value,
                  function() { return builders.testTypes.indexOf(value) != -1; });
              return true;
  
          case 'useTestData':
          case 'showAllRuns':
              this.crossDashboardState[key] = value == 'true';
              return true;
  
          default:
              return this._handleValidHashParameter(this, key, value);
          }
      },
      queryParameterValue: function(parameter)
      {
          return this.dashboardSpecificState[parameter] || this.crossDashboardState[parameter];
      },
      // Sets the page state. Takes varargs of key, value pairs.
      setQueryParameter: function(var_args)
      {
          var queryParamsAsState = {};
          for (var i = 0; i < arguments.length; i += 2) {
              var key = arguments[i];
              queryParamsAsState[key] = arguments[i + 1];
          }
  
          this.invalidateQueryParameters(queryParamsAsState);
  
          var newState = this._combinedDashboardState();
          for (var key in queryParamsAsState) {
              newState[key] = queryParamsAsState[key];
          }
  
          // Note: We use window.location.hash rather that window.location.replace
          // because of bugs in Chrome where extra entries were getting created
          // when back button was pressed and full page navigation was occurring.
          // FIXME: file those bugs.
          window.location.hash = this._permaLinkURLHash(newState);
      },
      toggleQueryParameter: function(param)
      {
          this.setQueryParameter(param, !this.queryParameterValue(param));
      },
      invalidateQueryParameters: function(queryParamsAsState)
      {
          for (var key in queryParamsAsState) {
              if (this._dashboardSpecificInvalidatingParameters && key in this._dashboardSpecificInvalidatingParameters)
                  delete this.dashboardSpecificState[this._dashboardSpecificInvalidatingParameters[key]];
          }
      },
      _joinParameters: function(stateObject)
      {
          var state = [];
          for (var key in stateObject) {
              var value = stateObject[key];
              if (value != this._defaultValue(key))
                  state.push(key + '=' + encodeURIComponent(value));
          }
          return state.join('&');
      },
      _permaLinkURLHash: function(opt_state)
      {
          var state = opt_state || this._combinedDashboardState();
          return '#' + this._joinParameters(state);
      },
      _combinedDashboardState: function()
      {
          var combinedState = Object.create(this.dashboardSpecificState);
          for (var key in this.crossDashboardState)
              combinedState[key] = this.crossDashboardState[key];
          return combinedState;
      },
      _defaultValue: function(key)
      {
          if (key in this._defaultDashboardSpecificStateValues)
              return this._defaultDashboardSpecificStateValues[key];
          return history.DEFAULT_CROSS_DASHBOARD_STATE_VALUES[key];
      },
      _handleLocationChange: function()
      {
          if (this.parseParameters())
              this._generatePage(this);
      }
  
  }
  
  })();
  