'use strict';

lazy_require = 'advisable' define [

'./states/index',
'./presenters/default',
'jquery.inview',
'stampit/stampit',
'observable',
lazy_require], (templates, presenter, inview, stampit, observable, advisable) ->

scopable = (widget) ->
  deferred = widget.sandbox.data.deferred()

  # TODO add widget plug-in as an extension for a widget
  require ['widgets/viewer/plugins/scopable'], (scopable) ->
    deferred.resolveWith scopable, [scopable widget]

  deferred

paginable = stampit
  flip_to: (page) ->
    @widget.scope.page (page - 1)
    @flip()

  flip: ->
    {scope} = @widget
    {page_number, total_pages} = scope

    return unless total_pages?

    # TODO set default abortion to decreatse page numbers amount
    scope.page ++page_number

    if page_number <= total_pages
      @widget.scope_to scope
    else
      @widget.sandbox.emit "#{@widget.name}.#{@widget.identifier}.last_page"
,
  {}
, ->

  {sandbox, scope}   = @widget
  {page_number}      = scope
  scope.total_pages ?= Infinity

  unless scope.page? page_number
    throw new TypeError "Pagination could not be initialized required method scope#page not found!"

  # TODO scope.subscribe 'page_number', total_pages

  sandbox.on "#{@widget.name}.#{@widget.identifier}.flip"         , @flip    , @
  sandbox.on "#{@widget.name}.#{@widget.identifier}.flip_to"      , @flip_to , @

  stampit.mixIn @, @widget.options.pagination

scrollable = stampit
  bottoned: ->
    scrollBottom     = @scroll_container.scrollTop() + @scroll_container.height()
    scrollableBottom = @widget.$el.height() + @widget.$el.offset().top

    scrollBottom + @buffer  > scrollableBottom

  scrolled: ->
    @widget.sandbox.emit "#{@widget.name}.#{@widget.identifier}.flip" if @bottoned()
,
  buffer: 400
, ->
  @scroll_container = $ window

  @scroll_container.scroll _.throttle (params...) =>
    @scrolled params...
  , 500

  # Trigger more items loading if page starts in bottom state
  # TODO Account for autofetchable viewer
  @widget.sandbox.on "viewer.#{@widget.identifier}.populated", @scrolled, @

  stampit.mixIn @, @widget.options.scroll

boo =
  cache: {}
  initialize: (container) ->
    container.children('.item')
      .each(@identify)
      .on('inview', @viewed)

  identify: (index, element) ->
    element  = $ element
    identity = _.uniqueId()
    element.data 'boo', identity
    boo.cache[identity] = element

  shame: (element) ->
    element  = $ element
    identity = element.data 'boo'
    child    = element.children()

    # We must store the current element state, we do so by storing a
    # custom tailored object in our object cache
    ghost =
      child: child
      shamed: true

    boo.cache[identity] = ghost

    element.css
      width: element.width()
      height: element.height()
      visibility: 'hidden'

    # child.detach()

  pride: (element) ->
    element  = $ element
    identity = element.data 'boo'
    ghost    = boo.cache[identity]

    if ghost and ghost.shamed
      ghost.shamed = false
      # In order to remove staticaly set width and height we pass
      # empty strings to css jquery method
      element.css width: '', height: '', visibility: ''

  viewed: (event, in_view, horizontal, vertical) ->
    boo[if in_view then 'pride' else 'shame'] event.target

# TODO Move each handler to independent features
handleable = stampit
  handleables:
    item:
      hover: (event, models) ->
        if event.type == 'mouseenter'
          @hover models.item
        else if event.type == 'mouseleave'
          @hover null
        else
          throw new TypeError 'viewer.handlers.hover: Event type incompatible with hovering.'

      clicked: (event, models) -> @select models.item

, {}, ->

  throw new TypeError "Widget property is mandatory for handleable stamp" unless @widget?

  @handlers =
    item:
      clicked: $.proxy @handleables.item.clicked, @widget
      hover  : $.proxy @handleables.item.hover  , @widget

  @
version: '0.2.5'

# TODO better separation of concerns
# TODO Current remote page that is beign displayed
options:
  resource: 'default'

  # TODO rename records to resources
  records: null

  # Automatically fetch records on initialization
  autofetch: false

  # If page attribute is set, viewer will assume that there is a
  # page method on the scope
  page: null

  scroll: null

type: 'Base'

presenter: presenter

select: (item) ->
  @selected_item?.selected = false
  @selected_item           = item
  item.selected            = true

  # We extend presentation.selected just to assign all values of the item model
  # TODO call presenter to do this job
  @sandbox.util.extend @presentation.selected   , item.model.json?() || item.model

  # TODO change paramters to item, item.model
  @sandbox.emit "viewer.#{@identifier}.selected", item.model

# Called when hover in and out from model
hover: (item) ->
  # TODO call presenter to do this job
  # @sandbox.util.extend @presentation.hovered   , item.model.json?() || item.model
  @sandbox.emit "viewer.#{@identifier}.hovered", item, item && item.model

scope_to: (scope, child_scope) ->
  # Singuralize in order to accept association scopes, since
  # association scopes return almost the same kind as of it's
  # singularized version
  sent_scope    = @inflector.singularize scope.resource.toString()
  current_scope = @inflector.singularize @scope.resource.toString()

  deferred      = @sandbox.data.deferred()

  if sent_scope != current_scope
    throw new TypeError "Invalid scope sent to viewer@#{@identifier} sent: '#{sent_scope}', expected: '#{current_scope}'"

  # For sobsequent usages we must store the scope
  @scope = scope

  @sandbox.emit "viewer.#{@identifier}.scope_changed", @scope

  # TODO better scope data binding, and updating
  if @view? and scope.scope?.data
    @view.update
      scope_data: observable scope.scope.data

  @repopulate()

# TODO rename this method
# TODO also move this to an external tag
statused: (status) ->
  if status
    @status = status
    @sandbox.emit "viewer.#{@identifier}.status_changed", status
  else
    @status

repopulate: ->
  unless @fetching?
    if @load?
      @load.stop()
      @load = null
  else
    @fetching?.abort?()

  # TODO store spinner instance, instead of creating a new one every time
  unless @load?
    @load   = @sandbox.ui.loader @$results

    # TODO implement status for viewer widget
    @statused 'loading'
    @$el.addClass 'idle'
    @$el.removeClass 'loading'

  {viewer}        = @presentation

  # ✔ Generalize this filtering option
  # TODO make scope.all method use scope too, and replace @scope.fetch by it
  options  = @options # TODO better options accessing
  @fetching = @scope.fetch null, (records) =>

    # TODO instantiate records before calling this callback
    records = _.map records, @resource, @resource unless records[0]?.resource or records[0]?.itemable

    # TODO implement Array.concat ou Array.merge in observer, and
    # use it here instead of overriding all records
    viewer.items = records

  @fetching.done (records) =>
    if viewer.items.length
      # boo.initialize @$el.find '.results .items'
      @$el.addClass 'filled'
      @$el.removeClass 'empty'
    else
      # TODO implement state support for viewer widget
      @$el.addClass 'empty'
      @$el.removeClass 'filled'

    @sandbox.emit "viewer.#{@identifier}.populated", records, @

    @fetching.always =>
      if @load?
        @load.stop()
        @load = null

      # TODO implement status for viewer widget
      @$el.removeClass 'loading'
      @statused 'idle'
      @$el.addClass 'idle'

populate: ->
  @load   = @sandbox.ui.loader @$el

  # TODO implement status for viewer widget
  @statused 'loading'
  @$el.removeClass 'idle'
  @$el.addClass 'loading'

  # TODO replace with strategy pattern, please!
  if @options.records?.length

    deferred = jQuery.Deferred()
    deferred.resolveWith @scope, [@options.records]

  else if @options.autofetch

    deferred = @scope.every()

  else

    deferred = jQuery.Deferred()
    deferred.resolveWith @scope, [[]]

  # Initialize dependencies
  # TODO replace with strategy pattern, please!
  @fetching = deferred.done (records) =>

    @load.stop()

    # TODO do not send records as parameter
    @presentation = @presenter records, @scope, @handleable

    # Initialize elements
    @$el.html templates[@options.resource]
    @$results = @$el.find '.results .items'

    if records.length
      # boo.initialize @$el.find '.results .items'
      @$el.addClass 'filled'
    else
      @$el.addClass 'empty'

    # TODO externalize parent presentation inheritation to a
    # elementless widget
    @inherit_parent_presentation()
    # TODO move binders to application
    # TODO on bind execute presentation_options method and extend and inherit from presenter what needed
    @bind @presentation, @sandbox.util.extend(true, @presenter.presentation, @options.presentation)

    @presentation.viewer.subscribe 'items', =>
      # Start possible widgets created by items with widget
      # instantiation markup
      @syncronize_children()

    # Start widgets that may have been created by bindings
    @sandbox.emit 'aura.sandbox.start', @sandbox
    @syncronize_children()

    @handles 'click', 'back', '.back'

    @sandbox.emit "viewer.#{@identifier}.populated", records, @

  deferred.always =>
    if @load?
      @load.stop()
      @load = null

    # TODO implement status for viewer widget
    @$el.removeClass 'loading'
    @statused 'idle'
    @$el.addClass 'idle'

  deferred.fail =>
    # TODO better error message and viewer status
    @html 'Failed to fetch data from server.'

plugins: (options) ->
  deferreds = [@]

  deferreds.push paginable  widget: @ if options.page
  deferreds.push scrollable widget: @ if options.scroll
  deferreds.push scopable   @         if options.scope or options.scopable

  @sandbox.data.when deferreds...

# TODO move this method to an extension
syncronize_children: ->
  @sandbox._children ||= []
  @sandbox._widget   ||= @

  # Add possible new childs
  @constructor.startAll(@$el).done (widgets...) =>
    for widget in widgets
      widget.sandbox._widget = widget
      widget.sandbox._parent = @sandbox

    @sandbox._children = @sandbox._children.concat widgets

    for widget in widgets
      # TODO emit this event only when all siblings have initialized
      @sandbox.emit "#{widget.name}.#{widget.identifier}.siblings_initialized", @sandbox._children

    true

  # TODO better internal aura widget selection
  # Prevent other child to be instantiated
  @$el.find('[data-aura-widget]').each (i, element) ->
    current = element.getAttribute 'data-aura-widget'
    element.removeAttribute 'data-aura-widget'
    element.setAttribute 'aura-widget', current

# TODO move this method to an extension
inherit_parent_presentation: ->
  return unless view = @sandbox?._parent?._view
  advisable view unless view.after?

  # TODO move this method to sandbox
  isDescendant = (parent, child) ->
    node = child.parentNode

    while (node != null)
      return true if (node == parent)
      node = node.parentNode

    false

  inherited = []
  # Copy default models
  # TODO think if its a good idea to notify about model name conflicts
  for name, model of view.models when not @presentation[name] # By default do not override child models with parent models
    @presentation[name] = model
    inherited.push name

  # TODO store bindings instead of searching every time
  for binding in view.bindings when binding.iterated
    for subview in binding.iterated
      if isDescendant subview.els[0], @$el.get(0)
        for name, model of subview.models when not @presentation[name]
          advisable subview
          @presentation[name] = model
          inherited.push name

        break

  # Schedule update of copied models
  view.after 'update', (models) =>
    @update_inherited_models view, models, inherited

  subview.after 'update', (models) =>
    @update_inherited_models view, models, inherited

  true

update_inherited_models: (parent, models, inherited) ->
  # Only update inherited models
  models = @sandbox.util._.pick models, inherited

  isDescendant = (parent, child) ->
    node = child.parentNode

    while (node != null)
      return true if (node == parent)
      node = node.parentNode

    false

  # Copy default models
  # TODO think if its a good idea to notify about model name conflicts
  for name, model of models
    @presentation[name] = model

  @view.update models

  # TODO store bindings instead of searching every time
  for binding in parent.bindings when binding.iterated
    for subview in binding.iterated
      if isDescendant subview.els[0], @$el.get(0)
        models = @sandbox.util._.pick subview.models, inherited
        @view.update models
        @presentation[name] = model for model in models
        break

  true

initialize: (options) ->
  # TODO import core extensions in another place
  @resource      = @sandbox.resource options.resource
  @scope         = @resource

  # Instantiate it's on handleable factory
  widget         = @
  widgetable     = stampit().enclose -> @widget = widget; @
  @handleable    = stampit.compose widgetable, handleable

  {sandbox: {util: {@inflector}}}   = @

  @sandbox.on "viewer.#{@identifier}.scope", @scope_to, @

  # Initalize plugins
  # TODO think how to implement plugins api
  loading = @plugins options

  @statused 'idle'
  @$el.addClass "viewer widget #{@inflector.cssify @identifier} idle clearfix"

  loading.done (widget) ->
    widget.require_custom options.resource

# TODO externalize this code to an extension
require_custom: (resource) ->
  deferred = @sandbox.data.deferred()

  # Fetch custom templates
  # TODO better custom templates structure and custom presenter
  # TODO better segregation of concerns on this code
  # TODO handle case where custom presenter does not exist!
  require [
    "text!./widgets/viewer/templates/default/#{resource}.html"
    "./widgets/viewer/presenters/#{resource}"
    ], (custom_default_template, custom_presenter) =>

    unless presenter.hasOwnProperty 'handlers'
      Object.defineProperty presenter, 'handlers',
        get: -> throw new Error "presenter.hanlder is deprecated, please compose upon handleable"
        set: -> throw new Error "presenter.hanlder is deprecated, please compose upon handleable"

    custom_default_template and templates[resource] = custom_default_template
    @presenter  = @sandbox.util.extend custom_presenter, presenter if custom_presenter

    # Fetch default data
    @populate()

    deferred.resolveWith @, [resource]

  , (error) =>
    # TODO handle other status codes with xhr error
    @sandbox.logger.error "Error when loading presenter and template for resource '#{resource}':\n\n", error.message + "\n\n", error
    deferred.rejectWith @, arguments

  deferred