/*
 * Copyright (C) 2011 Google Inc.  All rights reserved.
 * Copyright (C) 2006, 2007, 2008 Apple Inc.  All rights reserved.
 * Copyright (C) 2007 Matt Lilek (pewtermoose@gmail.com).
 * Copyright (C) 2009 Joseph Pecoraro
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * 1.  Redistributions of source code must retain the above copyright
 *     notice, this list of conditions and the following disclaimer.
 * 2.  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.
 * 3.  Neither the name of Apple Computer, Inc. ("Apple") 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 APPLE AND ITS 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 APPLE OR ITS 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.
 */
UI.highlightedSearchResultClassName = 'highlighted-search-result';
UI.highlightedCurrentSearchResultClassName = 'current-search-result';

/**
 * @param {!Element} element
 * @param {?function(!MouseEvent): boolean} elementDragStart
 * @param {function(!MouseEvent)} elementDrag
 * @param {?function(!MouseEvent)} elementDragEnd
 * @param {?string} cursor
 * @param {?string=} hoverCursor
 * @param {number=} startDelay
 */
UI.installDragHandle = function(
    element, elementDragStart, elementDrag, elementDragEnd, cursor, hoverCursor, startDelay) {
  /**
   * @param {!Event} event
   */
  function onMouseDown(event) {
    const dragHandler = new UI.DragHandler();
    const dragStart = dragHandler.elementDragStart.bind(
        dragHandler, element, elementDragStart, elementDrag, elementDragEnd, cursor, event);
    if (startDelay)
      startTimer = setTimeout(dragStart, startDelay);
    else
      dragStart();
  }

  function onMouseUp() {
    if (startTimer)
      clearTimeout(startTimer);
    startTimer = null;
  }

  let startTimer;
  element.addEventListener('mousedown', onMouseDown, false);
  if (startDelay)
    element.addEventListener('mouseup', onMouseUp, false);
  if (hoverCursor !== null)
    element.style.cursor = hoverCursor || cursor || '';
};

/**
 * @param {!Element} targetElement
 * @param {?function(!MouseEvent):boolean} elementDragStart
 * @param {function(!MouseEvent)} elementDrag
 * @param {?function(!MouseEvent)} elementDragEnd
 * @param {?string} cursor
 * @param {!Event} event
 */
UI.elementDragStart = function(targetElement, elementDragStart, elementDrag, elementDragEnd, cursor, event) {
  const dragHandler = new UI.DragHandler();
  dragHandler.elementDragStart(targetElement, elementDragStart, elementDrag, elementDragEnd, cursor, event);
};

/**
 * @unrestricted
 */
UI.DragHandler = class {
  constructor() {
    this._elementDragMove = this._elementDragMove.bind(this);
    this._elementDragEnd = this._elementDragEnd.bind(this);
    this._mouseOutWhileDragging = this._mouseOutWhileDragging.bind(this);
  }

  _createGlassPane() {
    this._glassPaneInUse = true;
    if (!UI.DragHandler._glassPaneUsageCount++) {
      UI.DragHandler._glassPane = new UI.GlassPane();
      UI.DragHandler._glassPane.setPointerEventsBehavior(UI.GlassPane.PointerEventsBehavior.BlockedByGlassPane);
      UI.DragHandler._glassPane.show(UI.DragHandler._documentForMouseOut);
    }
  }

  _disposeGlassPane() {
    if (!this._glassPaneInUse)
      return;
    this._glassPaneInUse = false;
    if (--UI.DragHandler._glassPaneUsageCount)
      return;
    UI.DragHandler._glassPane.hide();
    delete UI.DragHandler._glassPane;
    delete UI.DragHandler._documentForMouseOut;
  }

  /**
   * @param {!Element} targetElement
   * @param {?function(!MouseEvent):boolean} elementDragStart
   * @param {function(!MouseEvent)} elementDrag
   * @param {?function(!MouseEvent)} elementDragEnd
   * @param {?string} cursor
   * @param {!Event} event
   */
  elementDragStart(targetElement, elementDragStart, elementDrag, elementDragEnd, cursor, event) {
    // Only drag upon left button. Right will likely cause a context menu. So will ctrl-click on mac.
    if (event.button || (Host.isMac() && event.ctrlKey))
      return;

    if (this._elementDraggingEventListener)
      return;

    if (elementDragStart && !elementDragStart(/** @type {!MouseEvent} */ (event)))
      return;

    const targetDocument = event.target.ownerDocument;
    this._elementDraggingEventListener = elementDrag;
    this._elementEndDraggingEventListener = elementDragEnd;
    console.assert(
        (UI.DragHandler._documentForMouseOut || targetDocument) === targetDocument, 'Dragging on multiple documents.');
    UI.DragHandler._documentForMouseOut = targetDocument;
    this._dragEventsTargetDocument = targetDocument;
    try {
      this._dragEventsTargetDocumentTop = targetDocument.defaultView.top.document;
    } catch (e) {
      this._dragEventsTargetDocumentTop = this._dragEventsTargetDocument;
    }

    targetDocument.addEventListener('mousemove', this._elementDragMove, true);
    targetDocument.addEventListener('mouseup', this._elementDragEnd, true);
    targetDocument.addEventListener('mouseout', this._mouseOutWhileDragging, true);
    if (targetDocument !== this._dragEventsTargetDocumentTop)
      this._dragEventsTargetDocumentTop.addEventListener('mouseup', this._elementDragEnd, true);

    if (typeof cursor === 'string') {
      this._restoreCursorAfterDrag = restoreCursor.bind(this, targetElement.style.cursor);
      targetElement.style.cursor = cursor;
      targetDocument.body.style.cursor = cursor;
    }
    /**
     * @param {string} oldCursor
     * @this {UI.DragHandler}
     */
    function restoreCursor(oldCursor) {
      targetDocument.body.style.removeProperty('cursor');
      targetElement.style.cursor = oldCursor;
      this._restoreCursorAfterDrag = null;
    }
    event.preventDefault();
  }

  _mouseOutWhileDragging() {
    this._unregisterMouseOutWhileDragging();
    this._createGlassPane();
  }

  _unregisterMouseOutWhileDragging() {
    if (!UI.DragHandler._documentForMouseOut)
      return;
    UI.DragHandler._documentForMouseOut.removeEventListener('mouseout', this._mouseOutWhileDragging, true);
  }

  _unregisterDragEvents() {
    if (!this._dragEventsTargetDocument)
      return;
    this._dragEventsTargetDocument.removeEventListener('mousemove', this._elementDragMove, true);
    this._dragEventsTargetDocument.removeEventListener('mouseup', this._elementDragEnd, true);
    if (this._dragEventsTargetDocument !== this._dragEventsTargetDocumentTop)
      this._dragEventsTargetDocumentTop.removeEventListener('mouseup', this._elementDragEnd, true);
    delete this._dragEventsTargetDocument;
    delete this._dragEventsTargetDocumentTop;
  }

  /**
   * @param {!Event} event
   */
  _elementDragMove(event) {
    if (event.buttons !== 1) {
      this._elementDragEnd(event);
      return;
    }
    if (this._elementDraggingEventListener(/** @type {!MouseEvent} */ (event)))
      this._cancelDragEvents(event);
  }

  /**
   * @param {!Event} event
   */
  _cancelDragEvents(event) {
    this._unregisterDragEvents();
    this._unregisterMouseOutWhileDragging();

    if (this._restoreCursorAfterDrag)
      this._restoreCursorAfterDrag();

    this._disposeGlassPane();

    delete this._elementDraggingEventListener;
    delete this._elementEndDraggingEventListener;
  }

  /**
   * @param {!Event} event
   */
  _elementDragEnd(event) {
    const elementDragEnd = this._elementEndDraggingEventListener;
    this._cancelDragEvents(/** @type {!MouseEvent} */ (event));
    event.preventDefault();
    if (elementDragEnd)
      elementDragEnd(/** @type {!MouseEvent} */ (event));
  }
};

UI.DragHandler._glassPaneUsageCount = 0;

/**
 * @param {?Node=} node
 * @return {boolean}
 */
UI.isBeingEdited = function(node) {
  if (!node || node.nodeType !== Node.ELEMENT_NODE)
    return false;
  let element = /** {!Element} */ (node);
  if (element.classList.contains('text-prompt') || element.nodeName === 'INPUT' || element.nodeName === 'TEXTAREA')
    return true;

  if (!UI.__editingCount)
    return false;

  while (element) {
    if (element.__editing)
      return true;
    element = element.parentElementOrShadowHost();
  }
  return false;
};

/**
 * @return {boolean}
 * @suppressGlobalPropertiesCheck
 */
UI.isEditing = function() {
  if (UI.__editingCount)
    return true;

  const focused = document.deepActiveElement();
  if (!focused)
    return false;
  return focused.classList.contains('text-prompt') || focused.nodeName === 'INPUT' || focused.nodeName === 'TEXTAREA';
};

/**
 * @param {!Element} element
 * @param {boolean} value
 * @return {boolean}
 */
UI.markBeingEdited = function(element, value) {
  if (value) {
    if (element.__editing)
      return false;
    element.classList.add('being-edited');
    element.__editing = true;
    UI.__editingCount = (UI.__editingCount || 0) + 1;
  } else {
    if (!element.__editing)
      return false;
    element.classList.remove('being-edited');
    delete element.__editing;
    --UI.__editingCount;
  }
  return true;
};

// Avoids Infinity, NaN, and scientific notation (e.g. 1e20), see crbug.com/81165.
UI._numberRegex = /^(-?(?:\d+(?:\.\d+)?|\.\d+))$/;

UI.StyleValueDelimiters = ' \xA0\t\n"\':;,/()';

/**
 * @param {!Event} event
 * @return {?string}
 */
UI._valueModificationDirection = function(event) {
  let direction = null;
  if (event.type === 'mousewheel') {
    // When shift is pressed while spinning mousewheel, delta comes as wheelDeltaX.
    if (event.wheelDeltaY > 0 || event.wheelDeltaX > 0)
      direction = 'Up';
    else if (event.wheelDeltaY < 0 || event.wheelDeltaX < 0)
      direction = 'Down';
  } else {
    if (event.key === 'ArrowUp' || event.key === 'PageUp')
      direction = 'Up';
    else if (event.key === 'ArrowDown' || event.key === 'PageDown')
      direction = 'Down';
  }
  return direction;
};

/**
 * @param {string} hexString
 * @param {!Event} event
 * @return {?string}
 */
UI._modifiedHexValue = function(hexString, event) {
  const direction = UI._valueModificationDirection(event);
  if (!direction)
    return null;

  const mouseEvent = /** @type {!MouseEvent} */ (event);
  const number = parseInt(hexString, 16);
  if (isNaN(number) || !isFinite(number))
    return null;

  const hexStrLen = hexString.length;
  const channelLen = hexStrLen / 3;

  // Colors are either rgb or rrggbb.
  if (channelLen !== 1 && channelLen !== 2)
    return null;

  // Precision modifier keys work with both mousewheel and up/down keys.
  // When ctrl is pressed, increase R by 1.
  // When shift is pressed, increase G by 1.
  // When alt is pressed, increase B by 1.
  // If no shortcut keys are pressed then increase hex value by 1.
  // Keys can be pressed together to increase RGB channels. e.g trying different shades.
  let delta = 0;
  if (UI.KeyboardShortcut.eventHasCtrlOrMeta(mouseEvent))
    delta += Math.pow(16, channelLen * 2);
  if (mouseEvent.shiftKey)
    delta += Math.pow(16, channelLen);
  if (mouseEvent.altKey)
    delta += 1;
  if (delta === 0)
    delta = 1;
  if (direction === 'Down')
    delta *= -1;

  // Increase hex value by 1 and clamp from 0 ... maxValue.
  const maxValue = Math.pow(16, hexStrLen) - 1;
  const result = Number.constrain(number + delta, 0, maxValue);

  // Ensure the result length is the same as the original hex value.
  let resultString = result.toString(16).toUpperCase();
  for (let i = 0, lengthDelta = hexStrLen - resultString.length; i < lengthDelta; ++i)
    resultString = '0' + resultString;
  return resultString;
};

/**
 * @param {number} number
 * @param {!Event} event
 * @param {number=} modifierMultiplier
 * @return {?number}
 */
UI._modifiedFloatNumber = function(number, event, modifierMultiplier) {
  const direction = UI._valueModificationDirection(event);
  if (!direction)
    return null;

  const mouseEvent = /** @type {!MouseEvent} */ (event);

  // Precision modifier keys work with both mousewheel and up/down keys.
  // When ctrl is pressed, increase by 100.
  // When shift is pressed, increase by 10.
  // When alt is pressed, increase by 0.1.
  // Otherwise increase by 1.
  let delta = 1;
  if (UI.KeyboardShortcut.eventHasCtrlOrMeta(mouseEvent))
    delta = 100;
  else if (mouseEvent.shiftKey)
    delta = 10;
  else if (mouseEvent.altKey)
    delta = 0.1;

  if (direction === 'Down')
    delta *= -1;
  if (modifierMultiplier)
    delta *= modifierMultiplier;

  // Make the new number and constrain it to a precision of 6, this matches numbers the engine returns.
  // Use the Number constructor to forget the fixed precision, so 1.100000 will print as 1.1.
  const result = Number((number + delta).toFixed(6));
  if (!String(result).match(UI._numberRegex))
    return null;
  return result;
};

/**
 * @param {string} wordString
 * @param {!Event} event
 * @param {function(string, number, string):string=} customNumberHandler
 * @return {?string}
 */
UI.createReplacementString = function(wordString, event, customNumberHandler) {
  let prefix;
  let suffix;
  let number;
  let replacementString = null;
  let matches = /(.*#)([\da-fA-F]+)(.*)/.exec(wordString);
  if (matches && matches.length) {
    prefix = matches[1];
    suffix = matches[3];
    number = UI._modifiedHexValue(matches[2], event);
    if (number !== null)
      replacementString = prefix + number + suffix;
  } else {
    matches = /(.*?)(-?(?:\d+(?:\.\d+)?|\.\d+))(.*)/.exec(wordString);
    if (matches && matches.length) {
      prefix = matches[1];
      suffix = matches[3];
      number = UI._modifiedFloatNumber(parseFloat(matches[2]), event);
      if (number !== null) {
        replacementString =
            customNumberHandler ? customNumberHandler(prefix, number, suffix) : prefix + number + suffix;
      }
    }
  }
  return replacementString;
};

/**
 * @param {!Event} event
 * @param {!Element} element
 * @param {function(string,string)=} finishHandler
 * @param {function(string)=} suggestionHandler
 * @param {function(string, number, string):string=} customNumberHandler
 * @return {boolean}
 */
UI.handleElementValueModifications = function(event, element, finishHandler, suggestionHandler, customNumberHandler) {
  /**
   * @return {?Range}
   * @suppressGlobalPropertiesCheck
   */
  function createRange() {
    return document.createRange();
  }

  const arrowKeyOrMouseWheelEvent =
      (event.key === 'ArrowUp' || event.key === 'ArrowDown' || event.type === 'mousewheel');
  const pageKeyPressed = (event.key === 'PageUp' || event.key === 'PageDown');
  if (!arrowKeyOrMouseWheelEvent && !pageKeyPressed)
    return false;

  const selection = element.getComponentSelection();
  if (!selection.rangeCount)
    return false;

  const selectionRange = selection.getRangeAt(0);
  if (!selectionRange.commonAncestorContainer.isSelfOrDescendant(element))
    return false;

  const originalValue = element.textContent;
  const wordRange =
      selectionRange.startContainer.rangeOfWord(selectionRange.startOffset, UI.StyleValueDelimiters, element);
  const wordString = wordRange.toString();

  if (suggestionHandler && suggestionHandler(wordString))
    return false;

  const replacementString = UI.createReplacementString(wordString, event, customNumberHandler);

  if (replacementString) {
    const replacementTextNode = createTextNode(replacementString);

    wordRange.deleteContents();
    wordRange.insertNode(replacementTextNode);

    const finalSelectionRange = createRange();
    finalSelectionRange.setStart(replacementTextNode, 0);
    finalSelectionRange.setEnd(replacementTextNode, replacementString.length);

    selection.removeAllRanges();
    selection.addRange(finalSelectionRange);

    event.handled = true;
    event.preventDefault();

    if (finishHandler)
      finishHandler(originalValue, replacementString);

    return true;
  }
  return false;
};

/**
 * @param {number} ms
 * @param {number=} precision
 * @return {string}
 */
Number.preciseMillisToString = function(ms, precision) {
  precision = precision || 0;
  const format = '%.' + precision + 'f\xa0ms';
  return Common.UIString(format, ms);
};

/** @type {!Common.UIStringFormat} */
UI._microsFormat = new Common.UIStringFormat('%.0f\xa0\u03bcs');

/** @type {!Common.UIStringFormat} */
UI._subMillisFormat = new Common.UIStringFormat('%.2f\xa0ms');

/** @type {!Common.UIStringFormat} */
UI._millisFormat = new Common.UIStringFormat('%.0f\xa0ms');

/** @type {!Common.UIStringFormat} */
UI._secondsFormat = new Common.UIStringFormat('%.2f\xa0s');

/** @type {!Common.UIStringFormat} */
UI._minutesFormat = new Common.UIStringFormat('%.1f\xa0min');

/** @type {!Common.UIStringFormat} */
UI._hoursFormat = new Common.UIStringFormat('%.1f\xa0hrs');

/** @type {!Common.UIStringFormat} */
UI._daysFormat = new Common.UIStringFormat('%.1f\xa0days');

/**
 * @param {number} ms
 * @param {boolean=} higherResolution
 * @return {string}
 */
Number.millisToString = function(ms, higherResolution) {
  if (!isFinite(ms))
    return '-';

  if (ms === 0)
    return '0';

  if (higherResolution && ms < 0.1)
    return UI._microsFormat.format(ms * 1000);
  if (higherResolution && ms < 1000)
    return UI._subMillisFormat.format(ms);
  if (ms < 1000)
    return UI._millisFormat.format(ms);

  const seconds = ms / 1000;
  if (seconds < 60)
    return UI._secondsFormat.format(seconds);

  const minutes = seconds / 60;
  if (minutes < 60)
    return UI._minutesFormat.format(minutes);

  const hours = minutes / 60;
  if (hours < 24)
    return UI._hoursFormat.format(hours);

  const days = hours / 24;
  return UI._daysFormat.format(days);
};

/**
 * @param {number} seconds
 * @param {boolean=} higherResolution
 * @return {string}
 */
Number.secondsToString = function(seconds, higherResolution) {
  if (!isFinite(seconds))
    return '-';
  return Number.millisToString(seconds * 1000, higherResolution);
};

/**
 * @param {number} bytes
 * @return {string}
 */
Number.bytesToString = function(bytes) {
  if (bytes < 1024)
    return Common.UIString('%.0f\xa0B', bytes);

  const kilobytes = bytes / 1024;
  if (kilobytes < 100)
    return Common.UIString('%.1f\xa0KB', kilobytes);
  if (kilobytes < 1024)
    return Common.UIString('%.0f\xa0KB', kilobytes);

  const megabytes = kilobytes / 1024;
  if (megabytes < 100)
    return Common.UIString('%.1f\xa0MB', megabytes);
  else
    return Common.UIString('%.0f\xa0MB', megabytes);
};

/**
 * @param {number} num
 * @return {string}
 */
Number.withThousandsSeparator = function(num) {
  let str = num + '';
  const re = /(\d+)(\d{3})/;
  while (str.match(re))
    str = str.replace(re, '$1\xa0$2');  // \xa0 is a non-breaking space
  return str;
};

/**
 * @param {string} format
 * @param {?ArrayLike} substitutions
 * @return {!Element}
 */
UI.formatLocalized = function(format, substitutions) {
  const formatters = {s: substitution => substitution};
  /**
   * @param {!Element} a
   * @param {string|!Element} b
   * @return {!Element}
   */
  function append(a, b) {
    a.appendChild(typeof b === 'string' ? createTextNode(b) : b);
    return a;
  }
  return String.format(Common.UIString(format), substitutions, formatters, createElement('span'), append)
      .formattedResult;
};

/**
 * @return {string}
 */
UI.openLinkExternallyLabel = function() {
  return Common.UIString('Open in new tab');
};

/**
 * @return {string}
 */
UI.copyLinkAddressLabel = function() {
  return Common.UIString('Copy link address');
};

/**
 * @return {string}
 */
UI.anotherProfilerActiveLabel = function() {
  return Common.UIString('Another profiler is already active');
};

/**
 * @param {string|undefined} description
 * @return {string}
 */
UI.asyncStackTraceLabel = function(description) {
  if (description) {
    if (description === 'Promise.resolve')
      description = Common.UIString('Promise resolved');
    else if (description === 'Promise.reject')
      description = Common.UIString('Promise rejected');
    return description + ' ' + Common.UIString('(async)');
  }
  return Common.UIString('Async Call');
};

/**
 * @param {!Element} element
 */
UI.installComponentRootStyles = function(element) {
  UI.appendStyle(element, 'ui/inspectorCommon.css');
  UI.themeSupport.injectHighlightStyleSheets(element);
  UI.themeSupport.injectCustomStyleSheets(element);
  element.classList.add('platform-' + Host.platform());

  // Detect overlay scrollbar enable by checking for nonzero scrollbar width.
  if (!Host.isMac() && UI.measuredScrollbarWidth(element.ownerDocument) === 0)
    element.classList.add('overlay-scrollbar-enabled');
};

/**
 * @param {?Document} document
 * @return {number}
 */
UI.measuredScrollbarWidth = function(document) {
  if (typeof UI._measuredScrollbarWidth === 'number')
    return UI._measuredScrollbarWidth;
  if (!document)
    return 16;
  const scrollDiv = document.createElement('div');
  scrollDiv.setAttribute('style', 'width: 100px; height: 100px; overflow: scroll;');
  document.body.appendChild(scrollDiv);
  UI._measuredScrollbarWidth = scrollDiv.offsetWidth - scrollDiv.clientWidth;
  document.body.removeChild(scrollDiv);
  return UI._measuredScrollbarWidth;
};

/**
 * @param {!Element} element
 * @param {string=} cssFile
 * @return {!DocumentFragment}
 */
UI.createShadowRootWithCoreStyles = function(element, cssFile) {
  const shadowRoot = element.attachShadow({ mode: 'open' });
  UI.appendStyle(shadowRoot, 'ui/inspectorCommon.css');
  UI.themeSupport.injectHighlightStyleSheets(shadowRoot);
  UI.themeSupport.injectCustomStyleSheets(shadowRoot);
  if (cssFile)
    UI.appendStyle(shadowRoot, cssFile);
  shadowRoot.addEventListener('focus', UI._focusChanged.bind(UI), true);
  return shadowRoot;
};

/**
 * @param {!Document} document
 * @param {!Event} event
 */
UI._windowFocused = function(document, event) {
  if (event.target.document.nodeType === Node.DOCUMENT_NODE)
    document.body.classList.remove('inactive');
  UI._keyboardFocus = true;
  const listener = () => {
    const activeElement = document.deepActiveElement();
    if (activeElement)
      activeElement.removeAttribute('data-keyboard-focus');
    UI._keyboardFocus = false;
  };
  document.defaultView.requestAnimationFrame(() => {
    UI._keyboardFocus = false;
    document.removeEventListener('mousedown', listener, true);
  });
  document.addEventListener('mousedown', listener, true);

};

/**
 * @param {!Document} document
 * @param {!Event} event
 */
UI._windowBlurred = function(document, event) {
  if (event.target.document.nodeType === Node.DOCUMENT_NODE)
    document.body.classList.add('inactive');
};

/**
 * @param {!Event} event
 */
UI._focusChanged = function(event) {
  const document = event.target && event.target.ownerDocument;
  const element = document ? document.deepActiveElement() : null;
  UI.Widget.focusWidgetForNode(element);
  UI.XWidget.focusWidgetForNode(element);
  if (!UI._keyboardFocus)
    return;
  element.setAttribute('data-keyboard-focus', 'true');
  element.addEventListener('blur', () => element.removeAttribute('data-keyboard-focus'), {once: true, capture: true});
};

/**
 * @unrestricted
 */
UI.ElementFocusRestorer = class {
  /**
   * @param {!Element} element
   */
  constructor(element) {
    this._element = element;
    this._previous = element.ownerDocument.deepActiveElement();
    element.focus();
  }

  restore() {
    if (!this._element)
      return;
    if (this._element.hasFocus() && this._previous)
      this._previous.focus();
    this._previous = null;
    this._element = null;
  }
};

/**
 * @param {!Element} element
 * @param {number} offset
 * @param {number} length
 * @param {!Array.<!Object>=} domChanges
 * @return {?Element}
 */
UI.highlightSearchResult = function(element, offset, length, domChanges) {
  const result = UI.highlightSearchResults(element, [new TextUtils.SourceRange(offset, length)], domChanges);
  return result.length ? result[0] : null;
};

/**
 * @param {!Element} element
 * @param {!Array.<!TextUtils.SourceRange>} resultRanges
 * @param {!Array.<!Object>=} changes
 * @return {!Array.<!Element>}
 */
UI.highlightSearchResults = function(element, resultRanges, changes) {
  return UI.highlightRangesWithStyleClass(element, resultRanges, UI.highlightedSearchResultClassName, changes);
};

/**
 * @param {!Element} element
 * @param {string} className
 */
UI.runCSSAnimationOnce = function(element, className) {
  function animationEndCallback() {
    element.classList.remove(className);
    element.removeEventListener('webkitAnimationEnd', animationEndCallback, false);
  }

  if (element.classList.contains(className))
    element.classList.remove(className);

  element.addEventListener('webkitAnimationEnd', animationEndCallback, false);
  element.classList.add(className);
};

/**
 * @param {!Element} element
 * @param {!Array.<!TextUtils.SourceRange>} resultRanges
 * @param {string} styleClass
 * @param {!Array.<!Object>=} changes
 * @return {!Array.<!Element>}
 */
UI.highlightRangesWithStyleClass = function(element, resultRanges, styleClass, changes) {
  changes = changes || [];
  const highlightNodes = [];
  const textNodes = element.childTextNodes();
  const lineText = textNodes
                       .map(function(node) {
                         return node.textContent;
                       })
                       .join('');
  const ownerDocument = element.ownerDocument;

  if (textNodes.length === 0)
    return highlightNodes;

  const nodeRanges = [];
  let rangeEndOffset = 0;
  for (let i = 0; i < textNodes.length; ++i) {
    const range = {};
    range.offset = rangeEndOffset;
    range.length = textNodes[i].textContent.length;
    rangeEndOffset = range.offset + range.length;
    nodeRanges.push(range);
  }

  let startIndex = 0;
  for (let i = 0; i < resultRanges.length; ++i) {
    const startOffset = resultRanges[i].offset;
    const endOffset = startOffset + resultRanges[i].length;

    while (startIndex < textNodes.length &&
           nodeRanges[startIndex].offset + nodeRanges[startIndex].length <= startOffset)
      startIndex++;
    let endIndex = startIndex;
    while (endIndex < textNodes.length && nodeRanges[endIndex].offset + nodeRanges[endIndex].length < endOffset)
      endIndex++;
    if (endIndex === textNodes.length)
      break;

    const highlightNode = ownerDocument.createElement('span');
    highlightNode.className = styleClass;
    highlightNode.textContent = lineText.substring(startOffset, endOffset);

    const lastTextNode = textNodes[endIndex];
    const lastText = lastTextNode.textContent;
    lastTextNode.textContent = lastText.substring(endOffset - nodeRanges[endIndex].offset);
    changes.push({node: lastTextNode, type: 'changed', oldText: lastText, newText: lastTextNode.textContent});

    if (startIndex === endIndex) {
      lastTextNode.parentElement.insertBefore(highlightNode, lastTextNode);
      changes.push({node: highlightNode, type: 'added', nextSibling: lastTextNode, parent: lastTextNode.parentElement});
      highlightNodes.push(highlightNode);

      const prefixNode =
          ownerDocument.createTextNode(lastText.substring(0, startOffset - nodeRanges[startIndex].offset));
      lastTextNode.parentElement.insertBefore(prefixNode, highlightNode);
      changes.push({node: prefixNode, type: 'added', nextSibling: highlightNode, parent: lastTextNode.parentElement});
    } else {
      const firstTextNode = textNodes[startIndex];
      const firstText = firstTextNode.textContent;
      const anchorElement = firstTextNode.nextSibling;

      firstTextNode.parentElement.insertBefore(highlightNode, anchorElement);
      changes.push(
          {node: highlightNode, type: 'added', nextSibling: anchorElement, parent: firstTextNode.parentElement});
      highlightNodes.push(highlightNode);

      firstTextNode.textContent = firstText.substring(0, startOffset - nodeRanges[startIndex].offset);
      changes.push({node: firstTextNode, type: 'changed', oldText: firstText, newText: firstTextNode.textContent});

      for (let j = startIndex + 1; j < endIndex; j++) {
        const textNode = textNodes[j];
        const text = textNode.textContent;
        textNode.textContent = '';
        changes.push({node: textNode, type: 'changed', oldText: text, newText: textNode.textContent});
      }
    }
    startIndex = endIndex;
    nodeRanges[startIndex].offset = endOffset;
    nodeRanges[startIndex].length = lastTextNode.textContent.length;
  }
  return highlightNodes;
};

UI.applyDomChanges = function(domChanges) {
  for (let i = 0, size = domChanges.length; i < size; ++i) {
    const entry = domChanges[i];
    switch (entry.type) {
      case 'added':
        entry.parent.insertBefore(entry.node, entry.nextSibling);
        break;
      case 'changed':
        entry.node.textContent = entry.newText;
        break;
    }
  }
};

UI.revertDomChanges = function(domChanges) {
  for (let i = domChanges.length - 1; i >= 0; --i) {
    const entry = domChanges[i];
    switch (entry.type) {
      case 'added':
        entry.node.remove();
        break;
      case 'changed':
        entry.node.textContent = entry.oldText;
        break;
    }
  }
};

/**
 * @param {!Element} element
 * @param {?Element=} containerElement
 * @return {!UI.Size}
 */
UI.measurePreferredSize = function(element, containerElement) {
  const oldParent = element.parentElement;
  const oldNextSibling = element.nextSibling;
  containerElement = containerElement || element.ownerDocument.body;
  containerElement.appendChild(element);
  element.positionAt(0, 0);
  const result = element.getBoundingClientRect();

  element.positionAt(undefined, undefined);
  if (oldParent)
    oldParent.insertBefore(element, oldNextSibling);
  else
    element.remove();
  return new UI.Size(result.width, result.height);
};

/**
 * @unrestricted
 */
UI.InvokeOnceHandlers = class {
  /**
   * @param {boolean} autoInvoke
   */
  constructor(autoInvoke) {
    this._handlers = null;
    this._autoInvoke = autoInvoke;
  }

  /**
   * @param {!Object} object
   * @param {function()} method
   */
  add(object, method) {
    if (!this._handlers) {
      this._handlers = new Map();
      if (this._autoInvoke)
        this.scheduleInvoke();
    }
    let methods = this._handlers.get(object);
    if (!methods) {
      methods = new Set();
      this._handlers.set(object, methods);
    }
    methods.add(method);
  }

  /**
   * @suppressGlobalPropertiesCheck
   */
  scheduleInvoke() {
    if (this._handlers)
      requestAnimationFrame(this._invoke.bind(this));
  }

  _invoke() {
    const handlers = this._handlers;
    this._handlers = null;
    const keys = handlers.keysArray();
    for (let i = 0; i < keys.length; ++i) {
      const object = keys[i];
      const methods = handlers.get(object).valuesArray();
      for (let j = 0; j < methods.length; ++j)
        methods[j].call(object);
    }
  }
};

UI._coalescingLevel = 0;
UI._postUpdateHandlers = null;

UI.startBatchUpdate = function() {
  if (!UI._coalescingLevel++)
    UI._postUpdateHandlers = new UI.InvokeOnceHandlers(false);
};

UI.endBatchUpdate = function() {
  if (--UI._coalescingLevel)
    return;
  UI._postUpdateHandlers.scheduleInvoke();
  UI._postUpdateHandlers = null;
};

/**
 * @param {!Object} object
 * @param {function()} method
 */
UI.invokeOnceAfterBatchUpdate = function(object, method) {
  if (!UI._postUpdateHandlers)
    UI._postUpdateHandlers = new UI.InvokeOnceHandlers(true);
  UI._postUpdateHandlers.add(object, method);
};

/**
 * @param {!Window} window
 * @param {!Function} func
 * @param {!Array.<{from:number, to:number}>} params
 * @param {number} duration
 * @param {function()=} animationComplete
 * @return {function()}
 */
UI.animateFunction = function(window, func, params, duration, animationComplete) {
  const start = window.performance.now();
  let raf = window.requestAnimationFrame(animationStep);

  function animationStep(timestamp) {
    const progress = Number.constrain((timestamp - start) / duration, 0, 1);
    func(...params.map(p => p.from + (p.to - p.from) * progress));
    if (progress < 1)
      raf = window.requestAnimationFrame(animationStep);
    else if (animationComplete)
      animationComplete();
  }

  return () => window.cancelAnimationFrame(raf);
};

/**
 * @unrestricted
 */
UI.LongClickController = class extends Common.Object {
  /**
   * @param {!Element} element
   * @param {function(!Event)} callback
   */
  constructor(element, callback) {
    super();
    this._element = element;
    this._callback = callback;
    this._enable();
  }

  reset() {
    if (this._longClickInterval) {
      clearInterval(this._longClickInterval);
      delete this._longClickInterval;
    }
  }

  _enable() {
    if (this._longClickData)
      return;
    const boundMouseDown = mouseDown.bind(this);
    const boundMouseUp = mouseUp.bind(this);
    const boundReset = this.reset.bind(this);

    this._element.addEventListener('mousedown', boundMouseDown, false);
    this._element.addEventListener('mouseout', boundReset, false);
    this._element.addEventListener('mouseup', boundMouseUp, false);
    this._element.addEventListener('click', boundReset, true);

    this._longClickData = {mouseUp: boundMouseUp, mouseDown: boundMouseDown, reset: boundReset};

    /**
     * @param {!Event} e
     * @this {UI.LongClickController}
     */
    function mouseDown(e) {
      if (e.which !== 1)
        return;
      const callback = this._callback;
      this._longClickInterval = setTimeout(callback.bind(null, e), 200);
    }

    /**
     * @param {!Event} e
     * @this {UI.LongClickController}
     */
    function mouseUp(e) {
      if (e.which !== 1)
        return;
      this.reset();
    }
  }

  dispose() {
    if (!this._longClickData)
      return;
    this._element.removeEventListener('mousedown', this._longClickData.mouseDown, false);
    this._element.removeEventListener('mouseout', this._longClickData.reset, false);
    this._element.removeEventListener('mouseup', this._longClickData.mouseUp, false);
    this._element.addEventListener('click', this._longClickData.reset, true);
    delete this._longClickData;
  }
};

/**
 * @param {!Document} document
 * @param {!Common.Setting} themeSetting
 */
UI.initializeUIUtils = function(document, themeSetting) {
  document.body.classList.toggle('inactive', !document.hasFocus());
  document.defaultView.addEventListener('focus', UI._windowFocused.bind(UI, document), false);
  document.defaultView.addEventListener('blur', UI._windowBlurred.bind(UI, document), false);
  document.addEventListener('focus', UI._focusChanged.bind(UI), true);
  document.addEventListener('keydown', event => {
    UI._keyboardFocus = true;
    document.defaultView.requestAnimationFrame(() => void(UI._keyboardFocus = false));
  }, true);

  if (!UI.themeSupport)
    UI.themeSupport = new UI.ThemeSupport(themeSetting);
  UI.themeSupport.applyTheme(document);

  const body = /** @type {!Element} */ (document.body);
  UI.appendStyle(body, 'ui/inspectorStyle.css');
  UI.GlassPane.setContainer(/** @type {!Element} */ (document.body));
};

/**
 * @param {string} name
 * @return {string}
 */
UI.beautifyFunctionName = function(name) {
  return name || Common.UIString('(anonymous)');
};

var customElements = {}
/**
 * @param {string} localName
 * @param {string} typeExtension
 * @param {!Object} prototype
 * @return {function()}
 * @suppressGlobalPropertiesCheck
 * @template T
 */
UI.registerCustomElement = function(localName, typeExtension, prototype) {
  if (document.registerElement) {
    return document.registerElement(typeExtension, { prototype: Object.create(prototype), extends: localName });
  } else {
    customElements[typeExtension] = {
      name: localName,
      prototype: prototype
    }
    function ElementConstructor () {
      const element = document.createElement(typeExtension)
      return element
    }
    Object.defineProperty(ElementConstructor.prototype = prototype, 'constructor', {
      value: ElementConstructor
    })
    if (!document.__createElement) {
      document.__createElement = document.createElement
      Object.defineProperty(document, 'createElement', {
        configurable: true,
        value: function value (name, options) {
          var is = (options && options.is) || name
          var info = customElements[is]
          var element = document.__createElement(info ? info.name : is)
          if (info) {
            Object.setPrototypeOf(element, info.prototype)
            element.setAttribute('is', is)
          }
          if (typeof element.createdCallback === 'function') {
            element.createdCallback()
          }
          element.__setAttribute = element.setAttribute
          element.setAttribute = function(key, val) {
            if (typeof element.attributeChangedCallback === 'function') {
              var odlVal = element.getAttribute(key)
              element.__setAttribute(key, val)
              element.attributeChangedCallback(key, odlVal, val)
            } else {
              element.__setAttribute(key, val)
            }
          }
          return element
        }
      })
    }
    return ElementConstructor
  }
};

/**
 * @param {string} text
 * @param {function(!Event)=} clickHandler
 * @param {string=} className
 * @param {boolean=} primary
 * @return {!Element}
 */
UI.createTextButton = function(text, clickHandler, className, primary) {
  const element = createElementWithClass('button', className || '', 'text-button');
  element.textContent = text;
  if (primary)
    element.classList.add('primary-button');
  if (clickHandler)
    element.addEventListener('click', clickHandler, false);
  return element;
};

/**
 * @param {string=} className
 * @param {string=} type
 * @return {!Element}
 */
UI.createInput = function(className, type) {
  const element = createElementWithClass('input', className || '');
  element.spellcheck = false;
  element.classList.add('harmony-input');
  if (type)
    element.type = type;
  return element;
};

/**
 * @param {string} name
 * @param {string} title
 * @param {boolean=} checked
 * @return {!Element}
 */
UI.createRadioLabel = function(name, title, checked) {
  const element = createElement('label', 'dt-radio');
  element.radioElement.name = name;
  element.radioElement.checked = !!checked;
  element.createTextChild(title);
  return element;
};

/**
 * @param {string} title
 * @param {string} iconClass
 * @return {!Element}
 */
UI.createLabel = function(title, iconClass) {
  const element = createElement('label', 'dt-icon-label');
  element.createChild('span').textContent = title;
  element.type = iconClass;
  return element;
};

/**
 * @return {!Element}
 * @param {number} min
 * @param {number} max
 * @param {number} tabIndex
 */
UI.createSliderLabel = function(min, max, tabIndex) {
  const element = createElement('label', 'dt-slider');
  element.sliderElement.min = min;
  element.sliderElement.max = max;
  element.sliderElement.step = 1;
  element.sliderElement.tabIndex = tabIndex;
  return element;
};

/**
 * @param {!Node} node
 * @param {string} cssFile
 * @suppressGlobalPropertiesCheck
 */
UI.appendStyle = function(node, cssFile) {
  const content = Runtime.cachedResources[cssFile] || '';
  if (!content)
    console.error(cssFile + ' not preloaded. Check module.json');
  let styleElement = createElement('style');
  styleElement.type = 'text/css';
  styleElement.textContent = content;
  node.appendChild(styleElement);

  const themeStyleSheet = UI.themeSupport.themeStyleSheet(cssFile, content);
  if (themeStyleSheet) {
    styleElement = createElement('style');
    styleElement.type = 'text/css';
    styleElement.textContent = themeStyleSheet + '\n' + Runtime.resolveSourceURL(cssFile + '.theme');
    node.appendChild(styleElement);
  }
};

/**
 * @extends {HTMLLabelElement}
 */
UI.CheckboxLabel = class extends HTMLLabelElement {
  constructor() {
    super();
    /** @type {!DocumentFragment} */
    this._shadowRoot;
    /** @type {!HTMLInputElement} */
    this.checkboxElement;
    /** @type {!Element} */
    this.textElement;
    throw new Error('Checkbox must be created via factory method.');
  }

  /**
   * @override
   */
  createdCallback() {
    UI.CheckboxLabel._lastId = (UI.CheckboxLabel._lastId || 0) + 1;
    const id = 'ui-checkbox-label' + UI.CheckboxLabel._lastId;
    this._shadowRoot = UI.createShadowRootWithCoreStyles(this, 'ui/checkboxTextLabel.css');
    this.checkboxElement = /** @type {!HTMLInputElement} */ (this._shadowRoot.createChild('input'));
    this.checkboxElement.type = 'checkbox';
    this.checkboxElement.setAttribute('id', id);
    this.textElement = this._shadowRoot.createChild('label', 'dt-checkbox-text');
    this.textElement.setAttribute('for', id);
    this._shadowRoot.createChild('slot');
  }

  /**
   * @param {string=} title
   * @param {boolean=} checked
   * @param {string=} subtitle
   * @return {!UI.CheckboxLabel}
   */
  static create(title, checked, subtitle) {
    if (!UI.CheckboxLabel._constructor)
      UI.CheckboxLabel._constructor = UI.registerCustomElement('span', 'dt-checkbox', UI.CheckboxLabel.prototype);
    const element = /** @type {!UI.CheckboxLabel} */ (new UI.CheckboxLabel._constructor());
    element.checkboxElement.checked = !!checked;
    if (title !== undefined) {
      element.textElement.textContent = title;
      if (subtitle !== undefined)
        element.textElement.createChild('div', 'dt-checkbox-subtitle').textContent = subtitle;
    }
    return element;
  }

  /**
   * @param {string} color
   * @this {Element}
   */
  set backgroundColor(color) {
    this.checkboxElement.classList.add('dt-checkbox-themed');
    this.checkboxElement.style.backgroundColor = color;
  }

  /**
   * @param {string} color
   * @this {Element}
   */
  set checkColor(color) {
    this.checkboxElement.classList.add('dt-checkbox-themed');
    const stylesheet = createElement('style');
    stylesheet.textContent = 'input.dt-checkbox-themed:checked:after { background-color: ' + color + '}';
    this._shadowRoot.appendChild(stylesheet);
  }

  /**
   * @param {string} color
   * @this {Element}
   */
  set borderColor(color) {
    this.checkboxElement.classList.add('dt-checkbox-themed');
    this.checkboxElement.style.borderColor = color;
  }
};

(function() {
  UI.registerCustomElement('div', 'text-button', {
    /**
     * @this {Element}
     */
    createdCallback: function() {
      this.type = 'button';
      const root = UI.createShadowRootWithCoreStyles(this, 'ui/textButton.css');
      root.createChild('slot');
    },

    __proto__: HTMLButtonElement.prototype
  });

  UI.registerCustomElement('span', 'dt-radio', {
    /**
     * @this {Element}
     */
    createdCallback: function() {
      this.radioElement = this.createChild('input', 'dt-radio-button');
      this.radioElement.slot = 'dt-radio-button';
      this.radioElement.type = 'radio';
      const root = UI.createShadowRootWithCoreStyles(this, 'ui/radioButton.css');
      root.createChild('slot').name = 'dt-radio-button';
      root.createChild('slot');
      this.addEventListener('click', radioClickHandler, false);
    },

    __proto__: HTMLLabelElement.prototype
  });

  /**
   * @param {!Event} event
   * @suppressReceiverCheck
   * @this {Element}
   */
  function radioClickHandler(event) {
    if (this.radioElement.checked || this.radioElement.disabled)
      return;
    this.radioElement.checked = true;
    this.radioElement.dispatchEvent(new Event('change'));
  }

  UI.registerCustomElement('span', 'dt-icon-label', {
    /**
     * @this {Element}
     */
    createdCallback: function() {
      const root = UI.createShadowRootWithCoreStyles(this);
      this._iconElement = UI.Icon.create();
      this._iconElement.style.setProperty('margin-right', '4px');
      root.appendChild(this._iconElement);
      root.createChild('slot');
    },

    /**
     * @param {string} type
     * @this {Element}
     */
    set type(type) {
      this._iconElement.setIconType(type);
    },

    __proto__: HTMLLabelElement.prototype
  });

  UI.registerCustomElement('span', 'dt-slider', {
    /**
     * @this {Element}
     */
    createdCallback: function() {
      const root = UI.createShadowRootWithCoreStyles(this, 'ui/slider.css');
      this.sliderElement = createElementWithClass('input', 'dt-range-input');
      this.sliderElement.type = 'range';
      root.appendChild(this.sliderElement);
    },

    /**
     * @param {number} amount
     * @this {Element}
     */
    set value(amount) {
      this.sliderElement.value = amount;
    },

    /**
     * @this {Element}
     */
    get value() {
      return this.sliderElement.value;
    },

    __proto__: HTMLLabelElement.prototype
  });

  UI.registerCustomElement('span', 'dt-small-bubble', {
    /**
     * @this {Element}
     */
    createdCallback: function() {
      const root = UI.createShadowRootWithCoreStyles(this, 'ui/smallBubble.css');
      this._textElement = root.createChild('div');
      this._textElement.className = 'info';
      this._textElement.createChild('slot');
    },

    /**
     * @param {string} type
     * @this {Element}
     */
    set type(type) {
      this._textElement.className = type;
    },

    __proto__: HTMLLabelElement.prototype
  });

  UI.registerCustomElement('div', 'dt-close-button', {
    /**
     * @this {Element}
     */
    createdCallback: function() {
      const root = UI.createShadowRootWithCoreStyles(this, 'ui/closeButton.css');
      this._buttonElement = root.createChild('div', 'close-button');
      const regularIcon = UI.Icon.create('smallicon-cross', 'default-icon');
      this._hoverIcon = UI.Icon.create('mediumicon-red-cross-hover', 'hover-icon');
      this._activeIcon = UI.Icon.create('mediumicon-red-cross-active', 'active-icon');
      this._buttonElement.appendChild(regularIcon);
      this._buttonElement.appendChild(this._hoverIcon);
      this._buttonElement.appendChild(this._activeIcon);
    },

    /**
     * @param {boolean} gray
     * @this {Element}
     */
    set gray(gray) {
      if (gray) {
        this._hoverIcon.setIconType('mediumicon-gray-cross-hover');
        this._activeIcon.setIconType('mediumicon-gray-cross-active');
      } else {
        this._hoverIcon.setIconType('mediumicon-red-cross-hover');
        this._activeIcon.setIconType('mediumicon-red-cross-active');
      }
    },

    __proto__: HTMLDivElement.prototype
  });
})();

/**
 * @param {!Element} input
 * @param {function(string)} apply
 * @param {function(string):boolean} validate
 * @param {boolean} numeric
 * @param {number=} modifierMultiplier
 * @return {function(string)}
 */
UI.bindInput = function(input, apply, validate, numeric, modifierMultiplier) {
  input.addEventListener('change', onChange, false);
  input.addEventListener('input', onInput, false);
  input.addEventListener('keydown', onKeyDown, false);
  input.addEventListener('focus', input.select.bind(input), false);

  function onInput() {
    input.classList.toggle('error-input', !validate(input.value));
  }

  function onChange() {
    const valid = validate(input.value);
    input.classList.toggle('error-input', !valid);
    if (valid)
      apply(input.value);
  }

  /**
   * @param {!Event} event
   */
  function onKeyDown(event) {
    if (isEnterKey(event)) {
      if (validate(input.value))
        apply(input.value);
      event.preventDefault();
      return;
    }

    if (!numeric)
      return;

    const value = UI._modifiedFloatNumber(parseFloat(input.value), event, modifierMultiplier);
    const stringValue = value ? String(value) : '';
    if (!validate(stringValue) || !value)
      return;

    input.value = stringValue;
    apply(input.value);
    event.preventDefault();
  }

  /**
   * @param {string} value
   */
  function setValue(value) {
    if (value === input.value)
      return;
    const valid = validate(value);
    input.classList.toggle('error-input', !valid);
    input.value = value;
  }

  return setValue;
};

/**
 * @param {!CanvasRenderingContext2D} context
 * @param {string} text
 * @param {number} maxWidth
 * @param {function(string, number):string} trimFunction
 * @return {string}
 */
UI.trimText = function(context, text, maxWidth, trimFunction) {
  const maxLength = 200;
  if (maxWidth <= 10)
    return '';
  if (text.length > maxLength)
    text = trimFunction(text, maxLength);
  const textWidth = UI.measureTextWidth(context, text);
  if (textWidth <= maxWidth)
    return text;

  let l = 0;
  let r = text.length;
  let lv = 0;
  let rv = textWidth;
  while (l < r && lv !== rv && lv !== maxWidth) {
    const m = Math.ceil(l + (r - l) * (maxWidth - lv) / (rv - lv));
    const mv = UI.measureTextWidth(context, trimFunction(text, m));
    if (mv <= maxWidth) {
      l = m;
      lv = mv;
    } else {
      r = m - 1;
      rv = mv;
    }
  }
  text = trimFunction(text, l);
  return text !== '\u2026' ? text : '';
};

/**
 * @param {!CanvasRenderingContext2D} context
 * @param {string} text
 * @param {number} maxWidth
 * @return {string}
 */
UI.trimTextMiddle = function(context, text, maxWidth) {
  return UI.trimText(context, text, maxWidth, (text, width) => text.trimMiddle(width));
};

/**
 * @param {!CanvasRenderingContext2D} context
 * @param {string} text
 * @param {number} maxWidth
 * @return {string}
 */
UI.trimTextEnd = function(context, text, maxWidth) {
  return UI.trimText(context, text, maxWidth, (text, width) => text.trimEnd(width));
};

/**
 * @param {!CanvasRenderingContext2D} context
 * @param {string} text
 * @return {number}
 */
UI.measureTextWidth = function(context, text) {
  const maxCacheableLength = 200;
  if (text.length > maxCacheableLength)
    return context.measureText(text).width;

  let widthCache = UI.measureTextWidth._textWidthCache;
  if (!widthCache) {
    widthCache = new Map();
    UI.measureTextWidth._textWidthCache = widthCache;
  }
  const font = context.font;
  let textWidths = widthCache.get(font);
  if (!textWidths) {
    textWidths = new Map();
    widthCache.set(font, textWidths);
  }
  let width = textWidths.get(text);
  if (!width) {
    width = context.measureText(text).width;
    textWidths.set(text, width);
  }
  return width;
};

/**
 * @unrestricted
 */
UI.ThemeSupport = class {
  /**
   * @param {!Common.Setting} setting
   */
  constructor(setting) {
    this._themeName = setting.get() || 'default';
    this._themableProperties = new Set([
      'color', 'box-shadow', 'text-shadow', 'outline-color', 'background-image', 'background-color',
      'border-left-color', 'border-right-color', 'border-top-color', 'border-bottom-color', '-webkit-border-image',
      'fill', 'stroke'
    ]);
    /** @type {!Map<string, string>} */
    this._cachedThemePatches = new Map();
    this._setting = setting;
    this._customSheets = new Set();
  }

  /**
   * @return {boolean}
   */
  hasTheme() {
    return this._themeName !== 'default';
  }

  /**
   * @return {string}
   */
  themeName() {
    return this._themeName;
  }

  /**
   * @param {!Element} element
   */
  injectHighlightStyleSheets(element) {
    this._injectingStyleSheet = true;
    UI.appendStyle(element, 'ui/inspectorSyntaxHighlight.css');
    if (this._themeName === 'dark')
      UI.appendStyle(element, 'ui/inspectorSyntaxHighlightDark.css');
    this._injectingStyleSheet = false;
  }

   /**
   * @param {!Element|!ShadowRoot} element
   */
  injectCustomStyleSheets(element) {
    for (const sheet of this._customSheets){
      const styleElement = createElement('style');
      styleElement.type = 'text/css';
      styleElement.textContent = sheet;
      element.appendChild(styleElement);
    }
  }

  /**
   * @param {string} sheetText
   */
  addCustomStylesheet(sheetText) {
    this._customSheets.add(sheetText);
  }

  /**
   * @param {!Document} document
   */
  applyTheme(document) {
    if (!this.hasTheme())
      return;

    if (this._themeName === 'dark')
      document.documentElement.classList.add('-theme-with-dark-background');

    const styleSheets = document.styleSheets;
    const result = [];
    for (let i = 0; i < styleSheets.length; ++i)
      result.push(this._patchForTheme(styleSheets[i].href, styleSheets[i]));
    result.push('/*# sourceURL=inspector.css.theme */');

    const styleElement = createElement('style');
    styleElement.type = 'text/css';
    styleElement.textContent = result.join('\n');
    document.head.appendChild(styleElement);
  }

  /**
   * @param {string} id
   * @param {string} text
   * @return {string}
   * @suppressGlobalPropertiesCheck
   */
  themeStyleSheet(id, text) {
    if (!this.hasTheme() || this._injectingStyleSheet)
      return '';

    let patch = this._cachedThemePatches.get(id);
    if (!patch) {
      const styleElement = createElement('style');
      styleElement.type = 'text/css';
      styleElement.textContent = text;
      document.body.appendChild(styleElement);
      patch = this._patchForTheme(id, styleElement.sheet);
      document.body.removeChild(styleElement);
    }
    return patch;
  }

  /**
   * @param {string} id
   * @param {!StyleSheet} styleSheet
   * @return {string}
   */
  _patchForTheme(id, styleSheet) {
    const cached = this._cachedThemePatches.get(id);
    if (cached)
      return cached;

    try {
      const rules = styleSheet.cssRules;
      const result = [];
      for (let j = 0; j < rules.length; ++j) {
        if (rules[j] instanceof CSSImportRule) {
          result.push(this._patchForTheme(rules[j].styleSheet.href, rules[j].styleSheet));
          continue;
        }
        const output = [];
        const style = rules[j].style;
        const selectorText = rules[j].selectorText;
        for (let i = 0; style && i < style.length; ++i)
          this._patchProperty(selectorText, style, style[i], output);
        if (output.length)
          result.push(rules[j].selectorText + '{' + output.join('') + '}');
      }

      const fullText = result.join('\n');
      this._cachedThemePatches.set(id, fullText);
      return fullText;
    } catch (e) {
      this._setting.set('default');
      return '';
    }
  }

  /**
   * @param {string} selectorText
   * @param {!CSSStyleDeclaration} style
   * @param {string} name
   * @param {!Array<string>} output
   *
   * Theming API is primarily targeted at making dark theme look good.
   * - If rule has ".-theme-preserve" in selector, it won't be affected.
   * - One can create specializations for dark themes via body.-theme-with-dark-background selector in host context.
   */
  _patchProperty(selectorText, style, name, output) {
    if (!this._themableProperties.has(name))
      return;

    const value = style.getPropertyValue(name);
    if (!value || value === 'none' || value === 'inherit' || value === 'initial' || value === 'transparent')
      return;
    if (name === 'background-image' && value.indexOf('gradient') === -1)
      return;

    if (selectorText.indexOf('-theme-') !== -1)
      return;

    let colorUsage = UI.ThemeSupport.ColorUsage.Unknown;
    if (name.indexOf('background') === 0 || name.indexOf('border') === 0)
      colorUsage |= UI.ThemeSupport.ColorUsage.Background;
    if (name.indexOf('background') === -1)
      colorUsage |= UI.ThemeSupport.ColorUsage.Foreground;

    output.push(name);
    output.push(':');
    const items = value.replace(Common.Color.Regex, '\0$1\0').split('\0');
    for (let i = 0; i < items.length; ++i)
      output.push(this.patchColorText(items[i], colorUsage));
    if (style.getPropertyPriority(name))
      output.push(' !important');
    output.push(';');
  }

  /**
   * @param {string} text
   * @param {!UI.ThemeSupport.ColorUsage} colorUsage
   * @return {string}
   */
  patchColorText(text, colorUsage) {
    const color = Common.Color.parse(text);
    if (!color)
      return text;
    const outColor = this.patchColor(color, colorUsage);
    let outText = outColor.asString(null);
    if (!outText)
      outText = outColor.asString(outColor.hasAlpha() ? Common.Color.Format.RGBA : Common.Color.Format.RGB);
    return outText || text;
  }

  /**
   * @param {!Common.Color} color
   * @param {!UI.ThemeSupport.ColorUsage} colorUsage
   * @return {!Common.Color}
   */
  patchColor(color, colorUsage) {
    const hsla = color.hsla();
    this._patchHSLA(hsla, colorUsage);
    const rgba = [];
    Common.Color.hsl2rgb(hsla, rgba);
    return new Common.Color(rgba, color.format());
  }

  /**
   * @param {!Array<number>} hsla
   * @param {!UI.ThemeSupport.ColorUsage} colorUsage
   */
  _patchHSLA(hsla, colorUsage) {
    const hue = hsla[0];
    const sat = hsla[1];
    let lit = hsla[2];
    const alpha = hsla[3];

    switch (this._themeName) {
      case 'dark':
        const minCap = colorUsage & UI.ThemeSupport.ColorUsage.Background ? 0.14 : 0;
        const maxCap = colorUsage & UI.ThemeSupport.ColorUsage.Foreground ? 0.9 : 1;
        lit = 1 - lit;
        if (lit < minCap * 2)
          lit = minCap + lit / 2;
        else if (lit > 2 * maxCap - 1)
          lit = maxCap - 1 / 2 + lit / 2;

        break;
    }
    hsla[0] = Number.constrain(hue, 0, 1);
    hsla[1] = Number.constrain(sat, 0, 1);
    hsla[2] = Number.constrain(lit, 0, 1);
    hsla[3] = Number.constrain(alpha, 0, 1);
  }
};

/**
 * @enum {number}
 */
UI.ThemeSupport.ColorUsage = {
  Unknown: 0,
  Foreground: 1 << 0,
  Background: 1 << 1,
};

/**
 * @param {string} article
 * @param {string} title
 * @return {!Element}
 */
UI.createDocumentationLink = function(article, title) {
  return UI.XLink.create('https://developers.google.com/web/tools/chrome-devtools/' + article, title);
};

/**
 * @param {string} url
 * @return {!Promise<?Image>}
 */
UI.loadImage = function(url) {
  return new Promise(fulfill => {
    const image = new Image();
    image.addEventListener('load', () => fulfill(image));
    image.addEventListener('error', () => fulfill(null));
    image.src = url;
  });
};

/**
 * @param {?string} data
 * @return {!Promise<?Image>}
 */
UI.loadImageFromData = function(data) {
  return data ? UI.loadImage('data:image/jpg;base64,' + data) : Promise.resolve(null);
};

/** @type {!UI.ThemeSupport} */
UI.themeSupport;

/**
 * @param {function(!File)} callback
 * @return {!Node}
 */
UI.createFileSelectorElement = function(callback) {
  const fileSelectorElement = createElement('input');
  fileSelectorElement.type = 'file';
  fileSelectorElement.style.display = 'none';
  fileSelectorElement.setAttribute('tabindex', -1);
  fileSelectorElement.onchange = onChange;
  function onChange(event) {
    callback(fileSelectorElement.files[0]);
  }
  return fileSelectorElement;
};

/**
 * @const
 * @type {number}
 */
UI.MaxLengthForDisplayedURLs = 150;

UI.MessageDialog = class {
  /**
   * @param {string} message
   * @param {!Document|!Element=} where
   * @return {!Promise}
   */
  static async show(message, where) {
    const dialog = new UI.Dialog();
    dialog.setSizeBehavior(UI.GlassPane.SizeBehavior.MeasureContent);
    dialog.setDimmed(true);
    const shadowRoot = UI.createShadowRootWithCoreStyles(dialog.contentElement, 'ui/confirmDialog.css');
    const content = shadowRoot.createChild('div', 'widget');
    await new Promise(resolve => {
      const okButton = UI.createTextButton(Common.UIString('OK'), resolve, '', true);
      content.createChild('div', 'message').createChild('span').textContent = message;
      content.createChild('div', 'button').appendChild(okButton);
      dialog.setOutsideClickCallback(event => {
        event.consume();
        resolve();
      });
      dialog.show(where);
      okButton.focus();
    });
    dialog.hide();
  }
};

UI.ConfirmDialog = class {
  /**
   * @param {string} message
   * @param {!Document|!Element=} where
   * @return {!Promise<boolean>}
   */
  static async show(message, where) {
    const dialog = new UI.Dialog();
    dialog.setSizeBehavior(UI.GlassPane.SizeBehavior.MeasureContent);
    dialog.setDimmed(true);
    const shadowRoot = UI.createShadowRootWithCoreStyles(dialog.contentElement, 'ui/confirmDialog.css');
    const content = shadowRoot.createChild('div', 'widget');
    content.createChild('div', 'message').createChild('span').textContent = message;
    const buttonsBar = content.createChild('div', 'button');
    const result = await new Promise(resolve => {
      buttonsBar.appendChild(UI.createTextButton(Common.UIString('OK'), () => resolve(true), '', true));
      buttonsBar.appendChild(UI.createTextButton(Common.UIString('Cancel'), () => resolve(false)));
      dialog.setOutsideClickCallback(event => {
        event.consume();
        resolve(false);
      });
      dialog.show(where);
    });
    dialog.hide();
    return result;
  }
};

/**
 * @param {!UI.ToolbarToggle} toolbarButton
 * @return {!Element}
 */
UI.createInlineButton = function(toolbarButton) {
  const element = createElement('span');
  const shadowRoot = UI.createShadowRootWithCoreStyles(element, 'ui/inlineButton.css');
  element.classList.add('inline-button');
  const toolbar = new UI.Toolbar('');
  toolbar.appendToolbarItem(toolbarButton);
  shadowRoot.appendChild(toolbar.element);
  return element;
};

/**
 * @param {string} text
 * @param {number} maxLength
 * @return {!DocumentFragment}
 */
UI.createExpandableText = function(text, maxLength) {
  const fragment = createDocumentFragment();
  fragment.textContent = text.slice(0, maxLength);
  const hiddenText = text.slice(maxLength);

  const expandButton = fragment.createChild('span', 'expandable-inline-button');
  expandButton.setAttribute('data-text', ls`Show ${Number.withThousandsSeparator(hiddenText.length)} more`);
  expandButton.addEventListener('click', () => {
    if (expandButton.parentElement)
      expandButton.parentElement.insertBefore(createTextNode(hiddenText), expandButton);
    expandButton.remove();
  });

  const copyButton = fragment.createChild('span', 'expandable-inline-button');
  copyButton.setAttribute('data-text', ls`Copy`);
  copyButton.addEventListener('click', () => {
    InspectorFrontendHost.copyText(text);
  });
  return fragment;
};
