# TODO

Figure out how to prevent second focus on 'phase code' from changing value… class variable on focus on blur

class SelectOptions

REGEXP: /^\s*([\s\S]+?)(?:\s+as\s+([\s\S]+?))?(?:\s+group\s+by\s+([\s\S]+?))?(?:\s+disable\s+when\s+([\s\S]+?))?\s+for\s+(?:([\$\w][\$\w]*)|(?:\(\s*([\$\w][\$\w]*)\s*,\s*([\$\w][\$\w]*)\s*\)))\s+in\s+([\s\S]+?)(?:\s+track\s+by\s+([\s\S]+?))?$/;
    # 1: Model Value
    # 2: Display Value
    # 3: group by expression (groupByFn)
    # 4: disable when expression (disableWhenFn)
    # 5: Repeater
    # 6: object item key variable name
    # 7: object item value variable name
    # 8: collection
    # 9: track by expression
constructor: (@unparsed,@html) ->
  @filters  = @unparsed.split('|')
  @options  = @filters.shift()
  @match    = @options.match(@REGEXP)
  @raiseError() if !@match
raiseError: ->
  throw new Error(
    "Expected expression in form of " +
    "'_select_ (as _label_)? for (_key_,)?_value_ in _collection_'" +
    " but got '" + @unparsed + "'. Element: " + @html)

PrimitiveValueFunction = (s,l,a,i) ->

return s

class SelectFunctions

constructor: (match,parse,options) ->
  @repeater     = match[5]
  @collection   = parse(match[8])
  @modelValue   = match[1].replace(match[5] + '.','')
  @viewValue    = if match[2] then match[2].replace(match[5] + '.','') else @modelValue
  @modelValueFn = if @modelValue == match[5] then PrimitiveValueFunction else parse(@modelValue || @viewValue)
  @viewValueFn  = if @viewValue  == match[5] then PrimitiveValueFunction else parse(@viewValue || @modelValue)
  @isPrimative  =    @viewValue  == match[5]
  @altValue     = parse(options.showAs)

class MobileTemplate

constructor: (@element,functions) ->
  @template          = angular.element("<ul> </ul>")
  @search            = angular.element "<input type='search' placeholder='Search'>"
  @closeSearch       = angular.element "<button>X</button>"
  @tempHolder        = angular.element("<div class='filtered-select' ></div>")
  @body              = angular.element(document.body)
  @mousedownFunction = functions[0]
  @keydownFunction   = functions[1]
  @inputFunction     = functions[2]

  @bind()
  @stylize()
  @attachElements()
attachElements: ->
  @tempHolder.append @search
  @tempHolder.append @closeSearch
  @tempHolder.append @template
  @body.append @tempHolder
stylize: ->
  ulHeight = =>
    full = window.innerHeight
    full = full/2 if @element.hasClass('bottom')
    full - @search[0].offsetHeight + 'px'
  @tempHolder.addClass('bottom') if @element.hasClass('bottom')
  @template.css('height',ulHeight())
  @search.css('width','calc(100% - 50px)')
  @closeSearch.css('border','none').css('background-color','rgba(0,0,0,0.1)').css('width','30px').css('padding','5px')

bind: ->
  @element.bind 'touchstart', (event) =>
    touch = event.touches[event.touches.length - 1]
    @touchDetails = {
      startX: touch.screenX
      startY: touch.screenY
    }
  @element.bind 'touchend', (event) =>
    touch = event.changedTouches[event.changedTouches.length - 1]
    if Math.abs(@touchDetails.startX - touch.screenX) < 20 && Math.abs(@touchDetails.startY - touch.screenY) < 20
      @mousedownFunction(@tempHolder,@search,touch.clientY)
  @body.bind 'keydown', (event) =>
    @keydownFunction(@tempHolder,event)
  @search.bind 'input', (event) =>
    @inputFunction(event)
  @closeSearch. bind 'mousedown', (event) =>
    @tempHolder.removeClass('active')

class StandardTemplate

constructor: (@element,@attrs,functions,@disabled,@viewOptions,@mdInputContainer) ->
  @span            = angular.element "<span></span>"
  @search          = angular.element "<input class='autocomplete' type='search'>"
  @tempHolder      = angular.element "<div class='autocomplete menu md-whiteframe-z1'>"
  @template        = @tempHolder
  @typeAhead       = angular.element "<span style='position:absolute;'></span>"
  @inputFunction   = functions[0]
  @focusFunction   = functions[1]
  @blurFunction    = functions[2]
  @keydownFunction = functions[3]
  @attachElements()
  @stylize()
  @bind()
stylize: ->
  @search.css('width',@viewOptions.width || '100%')
  @search.addClass('md-input') if @element.hasClass('md-input')
  @search.css('color', 'black')
  @element.css('position','relative').css('overflow','visible')
  @span.css('overflow','hidden').css('width','100%').css('display','inline-block').css('position','relative')
  searchCss = window.getComputedStyle(@search[0])
  @tempHolder.css('display','none') if @viewOptions.hideList
  @typeAhead.css('white-space', 'nowrap')
  @typeAhead.css('padding-left', parseFloat(searchCss["padding-left"]) + parseFloat(searchCss["margin-left"]) + parseFloat(searchCss["border-left-width"]) + 'px')
  padding  = parseFloat(searchCss["padding-top"])  + parseFloat(searchCss["margin-top"])  + parseFloat(searchCss["border-bottom-width"])
  @typeAhead.css('padding-top',  padding + 'px')

bind: ->
  @search.bind 'input', (event)=>
    @inputFunction(@search,@typeAhead,event)
  @search.bind 'focus', (event)=>
    @mdInputContainer?.setFocused(true)
    @stylize()
    @focusFunction(@template,event)
  @search.bind 'blur', (event)=>
    @mdInputContainer?.setHasValue(@search.val())
    @mdInputContainer?.setFocused(false)
    @blurFunction(@search,@typeAhead,@template,event)
  @search.bind 'keydown', (event)=>
    @keydownFunction(@search,@typeAhead,@template,event)

attachElements: ->
  @span.append @typeAhead
  @span.append @search
  @element.append @span
  @element.append @tempHolder

class EventFunctions

constructor: (@functions,@buildTemplate,@updateValue,@filteredList,@filter,@timeout,@parse,@scope,@disabled,@options,@changeFn) ->

inputFunction: (search,typeAhead,event) =>
  location = search[0].selectionStart
  if location > (@options.minLength || 4) - 1
    if @functions.viewValueFn(@filteredList()[0])
      search.val(search.val()[0..location - 1])
      unless [8,46].includes(@keyholder)
        search.val(@functions.viewValueFn(@filteredList()[0]).replace(/^\s+/g,'')) if search.val().toLowerCase() == @functions.viewValueFn(@filteredList()[0]).replace(/^\s+/g,'')[0..location - 1].toLowerCase()
    else
      search.val(search.val()[0..location - 1].replace(/^\s+/g,''))
    search[0].setSelectionRange(location,location)
  else
    search.val(search.val()[0..2].replace(/^\s+/g,''))
  typeAhead.html(search.val()[0..location - 1])
  @buildTemplate()

focusFunction: (template,event) =>
  template.addClass('focused')

  @buildTemplate()
setTypeAheadScroll: (search,typeAhead) =>
  if search[0].offsetWidth < search[0].scrollWidth
    @timeout ->
      typeAhead.css('margin-left', '-' + search[0].scrollLeft + 'px')
    , 1
blurFunction: (search,typeAhead,template,event) =>
  template.removeClass('focused')
  @setTypeAheadScroll(search,typeAhead)
  if search.val().length < (@options.minLength || 4)
    @updateValue('')
  else
    if @functions.isPrimative
      obj = search.val()
    else
      obj={}
      obj[@functions.viewValue] = search.val()
    collection = @functions.collection(@scope)
    if @options.allowNew
      collection.push(obj) unless (if @functions.isPrimative then collection else collection.pluck(@functions.viewValue)).includes(obj[@functions.viewValue] || obj)
    val = @filteredList(!@options.allowNew,false,@options.allowNew,true)[0]
    @changeFn(@scope)(val) if @changeFn
    @updateValue @functions.modelValueFn(val)

keydownFunction: (search,typeAhead,template,input) =>
  @keyholder = input.keyCode
  @setTypeAheadScroll(search,typeAhead)
  keypress = (direction) ->
    index = if direction == 'next' then 0 else template.find('a').length - 1
    selected = angular.element(template[0].getElementsByClassName('active')[0])
    if selected.hasClass('active')
      selected.removeClass('active')
      until complete
        selected = angular.element(selected[0][direction + 'Sibling']) if selected[0]
        complete = !!selected[0]
        complete = selected[0].tagName == 'A' if complete
        complete = true if !selected[0]
    selected = angular.element(template.find('a')[index]) unless selected[0]
    ind = 0
    for el,i in template[0].getElementsByTagName('a')
      ind = i if el == selected[0]
    scroll = selected[0].scrollHeight * ind
    selected[0].parentElement.scrollTop = scroll
    selected.addClass('active')
    location = search[0].selectionStart
    search.val(selected.text().replace(/^\s+/g,''))
    search[0].setSelectionRange(location,location)
    typeAhead.html(selected.text()[0..location - 1])
  if input.keyCode == 40
    input.preventDefault()
    keypress('next')
  if input.keyCode == 38
    input.preventDefault()
    keypress('previous')
mkeydownFunction: (tempHolder,event) =>
  tempHolder.removeClass('active') if event.keyCode == 27
mousedownFunction: (tempHolder,search,clientY) =>
  return if tempHolder.hasClass('active')
  return if @disabled()
  search.val('')
  @buildTemplate()
  tempHolder.css('top',clientY)
  tempHolder.css('transition','none')
  @timeout ->
    tempHolder.css('transition','')
    tempHolder.css('top','')
    tempHolder.addClass('active')
minputFunction: (event) =>
  @buildTemplate()

standard: ->
  [@inputFunction,@focusFunction,@blurFunction,@keydownFunction]
mobile: ->
  [@mousedownFunction,@mkeydownFunction,@minputFunction]

angular.module('FilteredSelect', [])

.directive 'ngFilteredSelect', ($parse,$filter,$timeout)->
  require: ['ngModel', '?^mdInputContainer']
  link: (scope,element,attrs,controllers) ->
    ngModel = controllers[0]
    mdInputContainer = controllers[1]
    viewOptions = JSON.parse(attrs.filterOptions || '{}')
    equiv = (left,right) ->
      return true if left == right
      return true if (!!left && !!right) == false
      if !isNaN(left) && !isNaN(right)
        return true if parseFloat(left) == parseFloat(right)
      false
    filteredList = (similar,model,exact,full)->
      if similar
        bool = (left,right) ->
          return unless left
          !!left.match(new RegExp("^" + right.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&")))
      if exact
        bool = (left,right) -> left == right
      location = (if full then null else elements.search[0].selectionStart) || elements.search.val().length
      if functions.isPrimative
        obj = if model then scope.$eval(attrs.ngModel) else elements.search.val()[0..location - 1].replace(/^\s+/g,'') || ''
      else
        obj={}
        if model
          obj[functions.modelValue] = scope.$eval(attrs.ngModel)
        else
          obj[functions.viewValue] = elements.search.val()[0..location - 1].replace(/^\s+/g,'') || ''
      return unless functions.collection(scope)
      fList = $filter('orderBy')($filter('filter')(functions.collection(scope), obj,bool), viewOptions.orderBy || functions.viewValue)
      for filter in options.filters
        pieces = filter.replace(/\s+/,'').split(':')
        filterType = pieces.shift()
        value = pieces.join(':')
        fList = $filter(filterType)(fList, $parse(value)())
      fList
    buildTemplate = ->
      elements.template.empty()
      return unless filteredList()
      for item in filteredList()
        if isMobile
          li = angular.element '<li>' + functions.viewValueFn(item) + '</li>'
          touchDetails = {}
          li.bind 'touchstart', (event) =>
            touch = event.touches[event.touches.length - 1]
            touchDetails = {
              startX: touch.screenX
              startY: touch.screenY
            }
          li.bind 'touchend', (event) =>
            touch = event.changedTouches[event.changedTouches.length - 1]
            if Math.abs(touchDetails.startX - touch.screenX) < 20 && Math.abs(touchDetails.startY - touch.screenY) < 20
              val = (event.target.children[0].value)["to_" + event.target.children[0].getAttribute('ng-data-type')]()
              @clickedVal = val
              updateValue(val)
        else
          li = angular.element '<a class="item">' + functions.viewValueFn(item) + '</a>'
          li.bind 'mousedown', ($event)=>
            val = ($event.target.children[0].value)["to_" + $event.target.children[0].getAttribute('ng-data-type')]()
            @clickedVal = val
            updateValue(val)

        ip = angular.element '<input type="hidden">'
        ip.val(functions.modelValueFn(item))
        ip[0].setAttribute('ng-data-type',typeof functions.modelValueFn(item))
        li.append(ip)
        elements.template.append(li)
    setInitialValue = ->
      unless isMobile
        val = ''
        if model = scope.$eval(attrs.ngModel)
          viewScope = filteredList(true,true,true)[0]
          val = if viewScope then (functions.altValue(viewScope) || functions.viewValueFn(viewScope)).replace(/^\s+/g,'') else ''
        elements.search.val(val)
        elements.typeAhead.html(elements.search.val())
        mdInputContainer?.setHasValue(val)
      else
        unless model = scope.$eval(attrs.ngModel)
          view = attrs.placeholder
          element.css('color','rgba(0,0,0,0.4)')
        else
          element.css('color','')
          if functions.isPrimative
            obj = model
          else
            obj = {}
            obj[functions.modelValue] = model
          list = $filter('filter')(functions.collection(scope), obj,true)
          viewScope = list[0] if list
          view = if viewScope then functions.viewValueFn(viewScope) else attrs.placeholder
        element.html('')
        element.html(view)

    updateValue = (model) ->
      return if @clickedVal && @clickedVal != model
      $timeout =>
        delete @clickedVal
      , 300
      scope.$apply ->
        ngModel.$setViewValue(model || '')
        elements.tempHolder.removeClass('active')
        setInitialValue()
    disabled = ->
      unless typeof fieldset != 'undefined'
        done = false
        fieldset = element
        until done
          fieldset = fieldset.parent()
          unless done = typeof fieldset[0] == 'undefined'
            done = fieldset[0].tagName == 'FIELDSET'
      return true if typeof element[0].attributes.disabled != 'undefined' || $parse(attrs.ngDisabled)(scope)
      if fieldset.length > 0
        ngdis = if fieldset[0].attributes.ngDisabled then fieldset[0].attributes.ngDisabled.value else ''
        return true if fieldset[0].attributes.disabled || $parse(ngdis)(scope)
      form = element[0]
      until form.nodeName == 'FORM' || !form
        form = form.parentNode
        break if !form
      form ||= element[0]
      return true if form.disabled
      return false
    changeFn    = if attrs.fsChange then $parse(attrs.fsChange) else null
    options     = new SelectOptions(attrs.ngSelectOptions,element[0].outerHTML)
    functions   = new SelectFunctions(options.match,$parse,viewOptions)
    eFunctions  = new EventFunctions(functions,buildTemplate,updateValue,filteredList,$filter,$timeout,$parse,scope,disabled,viewOptions,changeFn)
    if isMobile = typeof attrs.ngMobile != 'undefined'
      elements  = new MobileTemplate(element,eFunctions.mobile())
    else
      elements  = new StandardTemplate(element,attrs,eFunctions.standard(),disabled(),viewOptions,mdInputContainer)
    mdInputContainer?.setHasPlaceholder(attrs.mdPlaceholder)
    mdInputContainer?.input = elements.search
    scope.$watchCollection functions.collection, (newVal,oldVal) ->
      return if newVal == oldVal
      setInitialValue()
      buildTemplate()
    scope.$watch attrs.ngModel, (newVal,oldVal) ->
      return if newVal == oldVal
      setInitialValue()
    scope.$watch disabled, (newVal) ->
      elements.search[0].disabled = newVal
    setInitialValue()
    buildTemplate()