# Default locale (en) strings for HTimeSheet
HLocale.components.HTimeSheet =
  strings:
    newItemLabel: 'New item'
HTimeSheet = HControl.extend

  componentName: 'timesheet'
  markupElemNames: [ 'label', 'value', 'timeline' ]

  defaultEvents:
    draggable:    true
    click:        true
    doubleClick:  true
    resize:       true

  controlDefaults: HControlDefaults.extend
    timeStart:          0 # 1970-01-01 00:00:00
    timeEnd:        86399 # 1970-01-01 23:59:59
    tzOffset:           0 # For custom timezone offsets in seconds; eg: 7200 => UTC+2
    itemMinHeight:     16 # Smallest allowed size for an item (in pixels)
    hideHours:      false # Enable to hide the hours in the gutter
    autoLabel:      false # Automatically set the label to the date, when enabled
    autoLabelFn: 'formatDate' # The name of the function to return formatted date/time
    notchesPerHour:     4 # by default 1/4 of an hour precision (15 minutes)
    snapToNotch:     true # Snaps time to nearest notch/line
    itemOffsetLeft:    64 # Theme settings; don't enter in options
    itemOffsetRight:    0 # Theme settings; don't enter in options
    itemOffsetTop:     20 # Theme settings; don't enter in options
    itemOffsetBottom:   0 # Theme settings; don't enter in options
    itemDisplayTime:         true  # Items display their time by default
    allowClickCreate:       false  # Enable to allow clicking in empty areas to create new items
    iconImage: 'timesheet_item_icons.png' # Icon resources for items
    allowDoubleClickCreate:  true  # Double-clicking empty areas are shortcuts for new items by default
    minDragSize:                5  # Minimum amount of pixels dragged required for accepting a drag
    hourOffsetTop:             -4  # Theme settings; don't enter in options

  customOptions: ( _opt )->
    @localeStrings = HLocale.components.HTimeSheet.strings
    _opt.defaultLabel = @localeStrings.newItemLabel unless _opt.defaultLabel?
    _opt.autoLabelFnOptions = { longWeekDay: true } unless _opt.autoLabelFnOptions?
    unless _opt.dummyValue?
      _opt.dummyValue =
        label: ''
        start: 0
        color: '#000000'

  themeSettings: ( _itemOffsetLeft, _itemOffsetTop, _itemOffsetRight, _itemOffsetBottom, _hourOffsetTop )->
    if @options.hideHours
      ELEM.addClassName( @elemId, 'nohours' )
      @options.itemOffsetLeft = 0
    else if _itemOffsetLeft?
      @options.itemOffsetLeft = _itemOffsetLeft
    @options.itemOffsetTop = _itemOffsetTop if _itemOffsetTop?
    @options.itemOffsetRight = _itemOffsetRight if _itemOffsetRight?
    @options.itemOffsetBottom = _itemOffsetBottom if _itemOffsetBottom?
    @options.hourOffsetTop = _hourOffsetTop if _hourOffsetTop?

  autoLabel: ->
    _locale = HLocale.dateTime
    _opt    = @options
    _label  = _locale[_opt.autoLabelFn]( _opt.timeStart, _opt.autoLabelFnOptions )
    if @label != _label
      @label = _label
      @refreshLabel()

  clearHours: ->
    for _hourItemId in @hourItems
      ELEM.del( _hourItemId )

  drawHours: ->
    _hourParent = @markupElemIds.timeline
    _lineParent = @markupElemIds.value
    _dateStart  = new Date( @options.timeStart * 1000 )
    _dateEnd    = new Date( @options.timeEnd * 1000 )
    _hourStart  = _dateStart.getUTCHours()
    _hourEnd    = _dateEnd.getUTCHours()
    _hours      = (_hourEnd - _hourStart) + 1
    _rectHeight = ELEM.getSize( _hourParent )[1]
    _topOffset  = @options.itemOffsetTop
    _height     = _rectHeight - _topOffset - @options.itemOffsetBottom
    _pxPerHour  = _height / _hours
    _notchesPerHour = @options.notchesPerHour
    _pxPerLine  = _pxPerHour / _notchesPerHour
    _bottomPos  = _rectHeight-_height-_topOffset-2
    _pxPerNotch = _pxPerHour / _notchesPerHour

    ELEM.setStyle( _hourParent, 'visibility', 'hidden', true )
    ELEM.setStyle( @markupElemIds.value, 'bottom', _bottomPos+'px' )

    @clearHours() if @hourItems?

    @itemOptions =
      notchHeight:  _pxPerNotch
      notches:      _hours * _notchesPerHour
      offsetTop:    _topOffset
      offsetBottom: _bottomPos
      height:       _height

    @hourItems = []

    for _hour in [_hourStart.._hourEnd]
      _lineTop  = Math.round( _topOffset + (_hour*_pxPerHour) )
      if _hour != _hourStart
        _hourTop = _lineTop + @options.hourOffsetTop
        @hourItems.push( ELEM.make( @markupElemIds.timeline, 'div',
          attr:
            className: 'hour'
          styles:
            top: _hourTop+'px'
          html: _hour+':00'
        ) )
        @hourItems.push( ELEM.make( _lineParent, 'div',
          attr:
            className: 'line'
          styles:
            top: (_lineTop+1)+'px'
            height: Math.round(_pxPerNotch-1)+'px'
        ) )
      for i in [1..._notchesPerHour] by 1
        _notchTop = Math.round(_lineTop + (_pxPerNotch*i))
        @hourItems.push( ELEM.make( _lineParent, 'div',
          attr:
            className: 'notch'
          styles:
            top: (_notchTop+1)+'px'
            height: Math.round(_pxPerNotch-1)+'px'
        ) )
    ELEM.setStyle( @markupElemIds.timeline, 'visibility', 'inherit' );

  # extra hook for refreshing; updates label and hours before doing common things
  refresh: ->
    if @drawn
      @autoLabel() if @options.autoLabel
      @drawHours()
    @base()

  # set the timezone offset (in seconds)
  setTzOffset: (_tzOffset)->
    @options.tzOffset = _tzOffset
    @refresh()

  # set the start timestamp of the timesheet
  setTimeStart: (_timeStart)->
    @options.timeStart = _timeStart
    @refresh()

  # set the end timestamp of the timesheet
  setTimeEnd: (_timeEnd)->
    @options.timeEnd = _timeEnd
    @refresh()

  # sets the range of timestams of the timesheet
  setTimeRange: (_timeRange)->
    if @typeChr(_timeRange) == 'a' and _timeRange.length == 2
      @setTimeStart( _timeRange[0] )
      @setTimeEnd(   _timeRange[1] )
    else if @typeChr(_timeRange) == 'h' and _timeRange.timeStart? and _timeRange.timeEnd?
      @setTimeStart( _timeRange.timeStart )
      @setTimeEnd(   _timeRange.timeEnd   )

  # sets the timestamp of the timesheet
  setDate: (_date)->
    @setTimeRange( [ _date, _date + @options.timeEnd - @options.timeStart ] )
    @refresh()

  # draw decorations
  drawSubviews: ->
    @drawHours()
    _options = @options
    _minDuration = Math.round(3600/_options.notchesPerHour)
    _dummyValue = @cloneObject( @options.dummyValue )
    _dummyValue.duration = _minDuration
    @dragPreviewRect = @rectFromValue(
      start:    _options.timeStart
      duration: _minDuration
    )
    @minDuration = _minDuration
    @dragPreview = HTimeSheetItem.new( @dragPreviewRect, @,
      value:        _dummyValue
      visible:      false
      iconImage:    @options.iconImage
      displayTime:  @options.itemDisplayTime
    )
    @dragPreview.setStyleOfPart('state','color','#fff')

  # event listener for clicks, simulates double clicks in case of not double click aware browser
  click: (x,y)->
    _prevClickTime = false
    _notCreated = not @clickCreated and not @doubleClickCreated and not @dragCreated
    if not @startDragTime and @prevClickTime
      _prevClickTime = @prevClickTime
    else if @startDragTime
      _prevClickTime = @startDragTime
    if _notCreated and @options.allowClickCreate
      @clickCreate( x,y )
      @clickCreated = true
      @doubleClickCreated = false
      @prevClickTime = false
    else if _notCreated and @options.allowDoubleClickCreate
      _currTime = new Date().getTime()
      if _prevClickTime
        _timeDiff = _currTime - _prevClickTime
      else
        _timeDiff = -1
      if _timeDiff > 150 and _timeDiff < 500 and not @doubleClickCreated
        @clickCreate( x, y )
        @clickCreated = false
        @doubleClickCreated = true
        @doubleClickSimCreated = true
      else
        @doubleClickCreated = false
      @prevClickTime = _currTime
    else
      @clickCreated = false
      @doubleClickCreated = false
      @prevClickTime = false

  # creates an item on click
  clickCreate: (x,y)->
    _startTime = @pxToTime( y-@pageY(), true )
    _endTime = _startTime + @minDuration
    @refreshDragPreview( _startTime, _endTime )
    @dragPreview.bringToFront()
    @dragPreview.show()
    if @activateEditor( @dragPreview )
      @editor.createItem( @cloneObject( @dragPreview.value ) )
    else
      @dragPreview.hide()

  # event listener for double clicks
  doubleClick: (x,y)->
    @prevClickTime = false
    @doubleClickCreated = false
    _notCreated = not @clickCreated and not @doubleClickCreated and not @doubleClickSimCreated and not @dragCreated
    if not @options.allowDoubleClickCreate and @options.allowClickCreate and _notCreated
      @click( x, y )
    else if @options.allowDoubleClickCreate and not @options.allowClickCreate and _notCreated
      @clickCreate( x, y )
      @clickCreated = false
      @doubleClickCreated = true
    else
      @clickCreated = false
    @doubleClickSimCreated = false

  # update the preview area
  refreshDragPreview: (_startTime, _endTime)->
    @dragPreviewRect.setTop( @timeToPx( _startTime ) )
    @dragPreviewRect.setBottom( @timeToPx( _endTime ) )
    @dragPreviewRect.setHeight( @options.itemMinHeight ) if @dragPreviewRect.height < @options.itemMinHeight
    @dragPreview.drawRect()
    @dragPreview.value.start = _startTime
    @dragPreview.value.duration = _endTime - _startTime
    @dragPreview.refreshValue()

  # drag & drop event listeners, used for dragging new timesheet items
  startDrag: (x,y)->
    @_startDragY = y
    @startDragTime = @pxToTime( y-@pageY(), true )
    @refreshDragPreview( @startDragTime, @startDragTime + @minDuration )
    @dragPreview.bringToFront()
    @dragPreview.show()
    true

  drag: (x,y)->
    _dragTime = @pxToTime( y-@pageY() )
    if _dragTime < @startDragTime
      _startTime = _dragTime
      _endTime = @startDragTime
    else
      _endTime = _dragTime
      _startTime = @startDragTime
    @refreshDragPreview( _startTime, _endTime )
    true

  endDrag: (x,y)->
    _dragTime = @pxToTime( y-@pageY() )
    _minDistanceSatisfied = Math.abs( @_startDragY - y ) >= @options.minDragSize
    @dragPreview.hide()
    if _dragTime != @startDragTime
      if _minDistanceSatisfied
        if @activateEditor( @dragPreview )
          @dragCreated = true
          @editor.createItem( @cloneObject( @dragPreview.value ) )
          return true
      @dragCreated = false
    else
      @dragCreated = false
      @clickCreated = false
      @startDragTime = false
      @click(x, y)
      return true
    false

  # a resize triggers refresh, of which the important part is refreshValue, which triggers redraw of the time sheet items
  resize: ->
    @base()
    @refresh()

  # snaps the time to grid
  snapTime: (_timeSecs,_begin)->
    _options = @options
    _pxDate = new Date( Math.round(_timeSecs) * 1000 )
    _snapSecs = Math.round( 3600 / _options.notchesPerHour )
    _halfSnapSecs = _snapSecs * 0.5
    _hourSecs = (_pxDate.getUTCMinutes()*60) + _pxDate.getUTCSeconds()
    _remSecs  = _hourSecs % _snapSecs
    if _begin
      _timeSecs -= _remSecs
    else
      if _remSecs > _halfSnapSecs
        _timeSecs += _snapSecs-_remSecs
      else
        _timeSecs -= _remSecs
    _timeSecs

  # snaps the pixel to grid
  snapPx: (_px)->
    _timeSecs = @pxToTime( _px )
    _timeSecs = @snapTime( _timeSecs )
    @timeToPx( _timeSecs )

  # activates the editor; _item is the timesheet item to edit
  activateEditor: (_item)->
    if @editor
      _editor = @editor
      _editor.setTimeSheetItem( _item )
      _item.bringToFront()
      _editor.bringToFront()
      _editor.show()
      return true
    false

  ###
  # = Description
  # Sets the editor given as parameter as the editor of instance.
  #
  # = Parameters
  # +_editor+::
  ###
  setEditor: (_editor)-> @editor = _editor

  ###
  # = Description
  # Destructor; destroys the editor first and commences inherited die.
  ###
  die: ->
    @editor.die() if @editor
    @editor = null
    @base()

  # converts pixels to time
  pxToTime: (_px, _begin)->
    _options = @options
    _timeStart = _options.timeStart
    _timeEnd   = _options.timeEnd
    _timeRange = _timeEnd - _timeStart
    _itemOptions = @itemOptions
    _top       = _itemOptions.offsetTop+1
    _height    = _itemOptions.height
    _pxPerSec  = _height / _timeRange
    _px -= _top
    _timeSecs  = _timeStart + ( _px / _pxPerSec )
    _timeSecs = @snapTime( _timeSecs, _begin ) if @options.snapToNotch
    if _timeSecs > _options.timeEnd
      _timeSecs = _options.timeEnd
    else if _timeSecs < _options.timeStart
      _timeSecs = _options.timeStart
    Math.round( _timeSecs )

  # converts time to pixels
  timeToPx: (_time, _begin)->
    _time = @snapTime( _time, _begin ) if @options.snapToNotch
    _options = @options
    _timeStart = _options.timeStart
    _timeEnd   = _options.timeEnd
    _time = _timeStart if _time < _timeStart
    _time = _timeEnd   if _time > _timeEnd
    _timeRange = _timeEnd - _timeStart
    _itemOptions = @itemOptions
    _top       = _itemOptions.offsetTop
    _height    = _itemOptions.height
    _pxPerSec  = _height / _timeRange
    _timeSecs  = _time - _timeStart
    _px        = _top + ( _timeSecs * _pxPerSec )
    Math.round( _px )

  # converts time to pixels for the rect
  rectFromValue: (_value)->
    _topPx = @timeToPx( _value.start )
    _bottomPx = @timeToPx( _value.start + _value.duration )
    _leftPx = @options.itemOffsetLeft
    _rightPx = @rect.width - @options.itemOffsetRight - 2
    if _topPx == 'underflow'
      _topPx = _itemOptions.offsetTop
    else if _topPx == 'overflow'
      return false
    if _bottomPx == 'underflow'
      return false
    else if _bottomPx == 'overflow'
      _bottomPx = _itemOptions.offsetTop + _itemOptions.height
    _rect = HRect.new( _leftPx, _topPx, _rightPx, _bottomPx )
    if _rect.height < @options.itemMinHeight
      _rect.setHeight( @options.itemMinHeight )
    _rect

  # creates a single time sheet item component
  createTimeSheetItem: (_value)->
    _rect = @rectFromValue( _value )
    return false if rect == false
    HTimeSheetItem.new( _rect, @,
      value: _value
      displayTime: @options.itemDisplayTime
      events:
        draggable: true
        doubleClick: true
    )

  # calls createTimeSheetItem with each value of the timesheet value array
  drawTimeSheetItems: ->
    _data = @value
    _items = @timeSheetItems
    if @typeChr(_data) == 'a' and _data.length > 0
      for _value in _data
        _item = @createTimeSheetItem( _value )
        _items.push( _item ) if _item

  ###
  # =Description
  # Create a new timeSheetItems if it hasn't been done already,
  # otherwise destroy the items of the old one before proceeding.
  ###
  _initTimeSheetItems: ->
    @timeSheetItems = [] unless @timeSheetItems?
    if @timeSheetItems.length > 0
      for _timeSheetItem in @timeSheetItems
        _timeSheetItem.die()
      @timeSheetItems = []

  # finds the index in the array which contains most sequential items
  _findLargestSequence: (_arr)->
    _index = 0
    _length = 1
    _maxLength = 1
    _bestIndex = 0
    for i in [1..._arr.length] by 1
      # grow:
      if _arr[i] - _arr[i-1] == 1 and _index == i-_length
        _length++
      # reset:
      else
        _index = i
        _length = 1
      if _length > _maxLength
        _bestIndex = _index
        _maxLength = _length
    [ _bestIndex, _maxLength ]

  # find the amount of overlapping time sheet items
  _findOverlapCount: (_items)->
    _overlaps = []
    _testRects = @_getTestRects( _items )
    for i in [0..._items.length] by 1
      _overlaps[i] = 0
    for i in [0...(_items.length-1)] by 1
      for j in [(i+1)..._items.length] by 1
        if _items[i].rect.intersects( _testRects[j] )
          _overlaps[i]++
          _overlaps[j]++
    Math.max.apply( Math, _overlaps )

  _getTestRects: (_items)->
    _rects = []
    for i in [0..._items.length] by 1
      _rects[i] = HRect.new( _items[i].rect )
      _rects[i].insetBy( 1, 1 )
    _rects

  # returns a sorted copy of the timeSheetItems array
  _sortedTimeSheetItems: (_sortFn)->
    unless _sortFn?
      _sortFn = (a,b)-> b.rect.height - a.rect.height
    _arr = []
    _items = @timeSheetItems
    _arr.push( _item ) for _item in _items
    _arr.sort(_sortFn)

  # Optimizes the left and right position of each timesheet item to fit
  # NOTE: This method will require refactoring; it's way too long and complicated
  _updateTimelineRects: ->
    # loop indexes:
    _options = @options
    _rect = @rect
    _availWidth = _rect.width - _options.itemOffsetRight - _options.itemOffsetLeft
    _left = _options.itemOffsetLeft
    # get a list of timesheet items sorted by height (larger to smaller order)
    _items = @_sortedTimeSheetItems()
    _itemCount = _items.length
    # amount of items ovelapping (max, actual number might be smaller after optimization)
    _overlapCount = @_findOverlapCount( _items )
    _width = Math.floor( _availWidth / (_overlapCount+1) )
    _maxCol = 0
    _origColById = []
    # No overlapping; nothing to do
    return false unless _overlapCount
    # move all items initially to one column right of the max overlaps
    _leftPos = _left+(_width*(_overlapCount+1))
    for i in [0..._itemCount] by 1
      _itemRect = _items[i].rect
      _itemRect.setLeft( _leftPos )
      _itemRect.setRight( _leftPos+_width )

    # optimize gaps by traversing each combination
    # and finding the first column with no gaps
    # the top-level loops three times in the following modes:
    # 0: place items into the first vacant column and find the actual max columns
    # 1: stretch columns to final column width
    # 2: stretch columns to fit multiple columns, if space is vacant
    for l in [0...3] by 1
      for i in [0..._itemCount] by 1
        _itemRect = _items[i].rect
        # in mode 1, just the column widths are changed
        if l == 1
          _leftPos = _left + (_origColById[i]*_width)
          _itemRect.setLeft( _leftPos )
          _itemRect.setRight( _leftPos + _width )
          continue
        _overlapCols = []
        _vacantCols = []
        _testRects = @_getTestRects( _items )
        _testRect = HRect.new( _itemRect )
        # test each column position (modes 0 and 2)
        for k in [0...(_overlapCount+1)] by 1
          _leftPos = _left + (k*_width)
          _testRect.setLeft( _leftPos )
          _testRect.setRight( _leftPos + _width )
          for j in [0..._itemCount] by 1
            if i != j and _testRect.intersects( _testRects[j] )
              _overlapCols.push( k ) unless ~_overlapCols.indexOf( k )
          if not ~_vacantCols.indexOf( k ) and not ~_overlapCols.indexOf( k )
            _vacantCols.push( k )

        # on the first run (mode 0) place items into the first column:
        if l == 0
          _origCol = _vacantCols[0]
          _origColById.push( _origCol )
          _leftPos = _left+(_origCol*_width)
          _rightPos = _leftPos + _width
          _maxCol = _origCol if _maxCol < _origCol
        else
          # on mode 2: stretch to fill multiple column widths,
          # because no item moving is done anymore at this stage, so we know what's free and what's not
          if _vacantCols.length > 0
            _optimalColAndLength = @_findLargestSequence( _vacantCols )
            _col = _vacantCols[ _optimalColAndLength[0] ]
            _colWidth = _optimalColAndLength[1]
          else
            _origCol = _origColById[i]
            _col = _origCol
            _colWidth = 1
          _leftPos = _left+(_col*_width)
          _rightPos = _leftPos+(_colWidth*_width)
        _itemRect.setLeft( _leftPos )
        _itemRect.setRight( _rightPos )
      # after the first run (mode 0) we know the actual amount of columns, so adjust column width accordingly
      if l == 0
        _overlapCount = _maxCol
        _width = Math.floor( _availWidth / (_maxCol+1) )
    true

  # draws the timeline (sub-routine of refreshValue)
  drawTimeline: ->
    @_initTimeSheetItems()
    @drawTimeSheetItems()
    @_updateTimelineRects()
    # use the dimensions of the views
    for _timeSheetItem in @timeSheetItems
      _timeSheetItem.drawRect()

  _sha: SHA.new(8)

  ###
  # Each item looks like this, any extra attributes are allowed,
  # but not used and not guaranteed to be preserved:
  #
  # { id: 'abcdef1234567890', # identifier, used in server to map id's
  #   label: 'Event title',   # label of event title
  #   start: 1299248619,      # epoch timestamp of event start
  #   duration: 3600,         # duration of event in seconds
  #   locked: true,           # when false, prevents editing the item
  #   icons: [ 1, 3, 6 ],     # icon id's to display
  #   color: '#ffffff'        # defaults to '#ffffff' if undefined
  # }
  #
  # = Description
  # Redraws and refreshes the values on timesheet.
  #
  ###
  refreshValue: ->
    return unless @itemOptions
    # optimization that ensures the rect and previous value are different before redrawing
    _valueStr = @encodeObject( @value )
    _rectStr = @rect.toString()
    _timeRangeStr = @options.timeStart+':'+@options.timeEnd
    _shaSum = @_sha.strSHA1( _valueStr+_rectStr+_timeRangeStr )
    if @_prevSum != _shaSum
      # the preview timesheet item is hidden when new data arrives (including what it created)
      @dragPreview.hide()
      @_prevSum = _shaSum
      @drawTimeline()