$ = (id) -> document.getElementById id
bgExclusions = chrome.extension.getBackgroundPage().Exclusions
# We have to use Settings from the background page here (not Settings, directly) to avoid a race condition for
# the page popup.  Specifically, we must ensure that the settings have been updated on the background page
# *before* the popup closes.  This ensures that any exclusion-rule changes are in place before the page
# regains the focus.
bgSettings = chrome.extension.getBackgroundPage().Settings
#
# Class hierarchy for various types of option.
class Option
  # Base class for all option classes.
  # Abstract. Option does not define @populateElement or @readValueFromElement.
  # Static. Array of all options.
  @all = []
  constructor: (@field,@onUpdated) ->
    @element = $(@field)
    @element.addEventListener "change", @onUpdated
    @fetch()
    Option.all.push this
  # Fetch a setting from localStorage, remember the @previous value and populate the DOM element.
  # Return the fetched value.
  fetch: ->
    @populateElement @previous = bgSettings.get @field
    @previous
  # Write this option's new value back to localStorage, if necessary.
  save: ->
    value = @readValueFromElement()
    if JSON.stringify(value) != JSON.stringify @previous
      bgSettings.set @field, @previous = value
  restoreToDefault: ->
    bgSettings.clear @field
    @fetch()
  # Static method.
  @saveOptions: ->
    Option.all.map (option) -> option.save()
  # Abstract method; only implemented in sub-classes.
  # Populate the option's DOM element (@element) with the setting's current value.
  # populateElement: (value) -> DO_SOMETHING
  # Abstract method; only implemented in sub-classes.
  # Extract the setting's new value from the option's DOM element (@element).
  # readValueFromElement: -> RETURN_SOMETHING
class NumberOption extends Option
  populateElement: (value) -> @element.value = value
  readValueFromElement: -> parseFloat @element.value
class TextOption extends Option
  constructor: (args...) ->
    super(args...)
    @element.addEventListener "input", @onUpdated
  populateElement: (value) -> @element.value = value
  readValueFromElement: -> @element.value.trim()
class NonEmptyTextOption extends Option
  constructor: (args...) ->
    super(args...)
    @element.addEventListener "input", @onUpdated
  populateElement: (value) -> @element.value = value
  # If the new value is not empty, then return it. Otherwise, restore the default value.
  readValueFromElement: -> if value = @element.value.trim() then value else @restoreToDefault()
class CheckBoxOption extends Option
  populateElement: (value) -> @element.checked = value
  readValueFromElement: -> @element.checked
class ExclusionRulesOption extends Option
  constructor: (args...) ->
    super(args...)
    $("exclusionAddButton").addEventListener "click", (event) =>
      @addRule()
  # Add a new rule, focus its pattern, scroll it into view, and return the newly-added element.  On the
  # options page, there is no current URL, so there is no initial pattern.  This is the default.  On the popup
  # page (see ExclusionRulesOnPopupOption), the pattern is pre-populated based on the current tab's URL.
  addRule: (pattern="") ->
      element = @appendRule { pattern: pattern, passKeys: "" }
      @getPattern(element).focus()
      exclusionScrollBox = $("exclusionScrollBox")
      exclusionScrollBox.scrollTop = exclusionScrollBox.scrollHeight
      @onUpdated()
      element
  populateElement: (rules) ->
    for rule in rules
      @appendRule rule
  # Append a row for a new rule.  Return the newly-added element.
  appendRule: (rule) ->
    content = document.querySelector('#exclusionRuleTemplate').content
    row = document.importNode content, true
    for field in ["pattern", "passKeys"]
      element = row.querySelector ".#{field}"
      element.value = rule[field]
      for event in [ "input", "change" ]
        element.addEventListener event, @onUpdated
    @getRemoveButton(row).addEventListener "click", (event) =>
      rule = event.target.parentNode.parentNode
      rule.parentNode.removeChild rule
      @onUpdated()
    @element.appendChild row
    @element.children[@element.children.length-1]
  readValueFromElement: ->
    rules =
      for element in @element.getElementsByClassName "exclusionRuleTemplateInstance"
        pattern: @getPattern(element).value.trim()
        passKeys: @getPassKeys(element).value.trim()
    rules.filter (rule) -> rule.pattern
  # Accessors for the three main sub-elements of an "exclusionRuleTemplateInstance".
  getPattern: (element) -> element.querySelector(".pattern")
  getPassKeys: (element) -> element.querySelector(".passKeys")
  getRemoveButton: (element) -> element.querySelector(".exclusionRemoveButton")
# ExclusionRulesOnPopupOption is ExclusionRulesOption, extended with some UI tweeks suitable for use in the
# page popup.  This also differs from ExclusionRulesOption in that, on the page popup, there is always a URL
# (@url) associated with the current tab.
class ExclusionRulesOnPopupOption extends ExclusionRulesOption
  constructor: (@url, args...) ->
    super(args...)
  addRule: ->
    element = super @generateDefaultPattern()
    @activatePatternWatcher element
    # ExclusionRulesOption.addRule()/super() has focused the pattern.  Here, focus the passKeys instead;
    # because, in the popup, we already have a pattern, so the user is more likely to edit the passKeys.
    @getPassKeys(element).focus()
    # Return element (for consistency with ExclusionRulesOption.addRule()).
    element
  populateElement: (rules) ->
    super(rules)
    elements = @element.getElementsByClassName "exclusionRuleTemplateInstance"
    @activatePatternWatcher element for element in elements
    haveMatch = false
    for element in elements
      pattern = @getPattern(element).value.trim()
      if 0 <= @url.search bgExclusions.RegexpCache.get pattern
        haveMatch = true
        @getPassKeys(element).focus()
      else
        element.style.display = 'none'
    @addRule() unless haveMatch
  # Provide visual feedback (make it red) when a pattern does not match the current tab's URL.
  activatePatternWatcher: (element) ->
    patternElement = element.children[0].firstChild
    patternElement.addEventListener "keyup", =>
      if @url.match bgExclusions.RegexpCache.get patternElement.value
        patternElement.title = patternElement.style.color = ""
      else
        patternElement.style.color = "red"
        patternElement.title = "Red text means that the pattern does not\nmatch the current URL."
  # Generate a default exclusion-rule pattern from a URL.  This is then used to pre-populate the pattern on
  # the page popup.
  generateDefaultPattern: ->
    if /^https?:\/\/./.test @url
      # The common use case is to disable Vimium at the domain level.
      # Generate "https?://www.example.com/*" from "http://www.example.com/path/to/page.html".
      "https?:/" + @url.split("/",3)[1..].join("/") + "/*"
    else if /^[a-z]{3,}:\/\/./.test @url
      # Anything else which seems to be a URL.
      @url.split("/",3).join("/") + "/*"
    else
      @url + "*"
Options =
  exclusionRules: ExclusionRulesOption
  filterLinkHints: CheckBoxOption
  waitForEnterForFilteredHints: CheckBoxOption
  hideHud: CheckBoxOption
  keyMappings: TextOption
  linkHintCharacters: NonEmptyTextOption
  linkHintNumbers: NonEmptyTextOption
  newTabUrl: NonEmptyTextOption
  nextPatterns: NonEmptyTextOption
  previousPatterns: NonEmptyTextOption
  regexFindMode: CheckBoxOption
  scrollStepSize: NumberOption
  smoothScroll: CheckBoxOption
  grabBackFocus: CheckBoxOption
  searchEngines: TextOption
  searchUrl: NonEmptyTextOption
  userDefinedLinkHintCss: TextOption
initOptionsPage = ->
  onUpdated = ->
    $("saveOptions").removeAttribute "disabled"
    $("saveOptions").innerHTML = "Save Changes"
  # Display either "linkHintNumbers" or "linkHintCharacters", depending upon "filterLinkHints".
  maintainLinkHintsView = ->
    hide = (el) -> el.style.display = "none"
    show = (el) -> el.style.display = "table-row"
    if $("filterLinkHints").checked
      hide $("linkHintCharactersContainer")
      show $("linkHintNumbersContainer")
      show $("waitForEnterForFilteredHintsContainer")
    else
      show $("linkHintCharactersContainer")
      hide $("linkHintNumbersContainer")
      hide $("waitForEnterForFilteredHintsContainer")
  maintainAdvancedOptions = ->
    if bgSettings.get "optionsPage_showAdvancedOptions"
      $("advancedOptions").style.display = "table-row-group"
      $("advancedOptionsButton").innerHTML = "Hide Advanced Options"
    else
      $("advancedOptions").style.display = "none"
      $("advancedOptionsButton").innerHTML = "Show Advanced Options"
  maintainAdvancedOptions()
  toggleAdvancedOptions = (event) ->
    bgSettings.set "optionsPage_showAdvancedOptions", not bgSettings.get "optionsPage_showAdvancedOptions"
    maintainAdvancedOptions()
    $("advancedOptionsButton").blur()
    event.preventDefault()
  activateHelpDialog = ->
    HelpDialog.toggle showAllCommandDetails: true
  saveOptions = ->
    $("linkHintCharacters").value = $("linkHintCharacters").value.toLowerCase()
    Option.saveOptions()
    $("saveOptions").disabled = true
    $("saveOptions").innerHTML = "No Changes"
  $("saveOptions").addEventListener "click", saveOptions
  $("advancedOptionsButton").addEventListener "click", toggleAdvancedOptions
  $("showCommands").addEventListener "click", activateHelpDialog
  $("filterLinkHints").addEventListener "click", maintainLinkHintsView
  for element in document.getElementsByClassName "nonEmptyTextOption"
    element.className = element.className + " example info"
    element.innerHTML = "Leave empty to reset this option."
  window.onbeforeunload = -> "You have unsaved changes to options." unless $("saveOptions").disabled
  document.addEventListener "keyup", (event) ->
    if event.ctrlKey and event.keyCode == 13
      document.activeElement.blur() if document?.activeElement?.blur
      saveOptions()
  # Populate options. The constructor adds each new object to "Option.all".
  for own name, type of Options
    new type(name,onUpdated)
  maintainLinkHintsView()
initPopupPage = ->
  chrome.tabs.query { active: true, currentWindow: true }, ([tab]) ->
    exclusions = null
    document.getElementById("optionsLink").setAttribute "href", chrome.runtime.getURL("pages/options.html")
    # As the active URL, we choose the most recently registered URL from a frame in the tab, or the tab's own
    # URL.
    url = chrome.extension.getBackgroundPage().urlForTab[tab.id] || tab.url
    updateState = ->
      rule = bgExclusions.getRule url, exclusions.readValueFromElement()
      $("state").innerHTML = "Vimium will " +
        if rule and rule.passKeys
          "exclude #{rule.passKeys}"
        else if rule
          "be disabled"
        else
          "be enabled"
    onUpdated = ->
      $("helpText").innerHTML = "Type Ctrl-Enter to save and close."
      $("saveOptions").removeAttribute "disabled"
      $("saveOptions").innerHTML = "Save Changes"
      updateState() if exclusions
    saveOptions = ->
      Option.saveOptions()
      $("saveOptions").innerHTML = "Saved"
      $("saveOptions").disabled = true
    $("saveOptions").addEventListener "click", saveOptions
    document.addEventListener "keyup", (event) ->
      if event.ctrlKey and event.keyCode == 13
        saveOptions()
        window.close()
    # Populate options. Just one, here.
    exclusions = new ExclusionRulesOnPopupOption url, "exclusionRules", onUpdated
    updateState()
    document.addEventListener "keyup", updateState
#
# Initialization.
document.addEventListener "DOMContentLoaded", ->
  xhr = new XMLHttpRequest()
  xhr.open 'GET', chrome.extension.getURL('pages/exclusions.html'), true
  xhr.onreadystatechange = ->
    if xhr.readyState == 4
      $("exclusionScrollBox").innerHTML = xhr.responseText
      switch location.pathname
        when "/pages/options.html" then initOptionsPage()
        when "/pages/popup.html" then initPopupPage()
  xhr.send()
# Exported for tests.
root = exports ? window
extend root, {Options, isVimiumOptionsPage: true}