class @BatFire
  @VERSION = '0.1.0'

class BatFire.Reference
  constructor: ({@path, @parent}) ->
    if @parent
      @ref = @parent.child(@path)
    else
      @ref = new Firebase(@path)

  child: (path) ->
    @ref.child(path)

BatFire.AppMixin =
  initialize: ->
    @syncsWithFirebase = (@firebaseAppName) ->
      @firebaseURL = "https://#{@firebaseAppName}.firebaseio.com/BatFire"
      @set 'firebase', new BatFire.Reference(path: @firebaseURL)

    @syncs = (keypathString, {as}={}) ->
      @_syncKeypaths ?= []
      @_syncKeypaths.push(keypathString)
      firebasePath = keypathString.replace(/\./, '/')
      childRef = @get('firebase').child("syncs/#{firebasePath}")
      syncConstructorName = as
      @observe keypathString, (newValue, oldValue) =>
        return if newValue is oldValue or Batman.typeOf(newValue) is 'Undefined'
        newValue = newValue.toJSON() if newValue?.toJSON
        childRef.set(newValue)
      childRef.on 'value', (snapshot) =>
        value = snapshot.val()
        if syncConstructorName?
          syncConstructor = Batman.currentApp[syncConstructorName]
          value = new syncConstructor(value)
        @set(keypathString, value)

    @_updateFirebaseChild = (keypathString, newValue) ->
      firebasePath = keypathString.replace(/\./, '/')
      childRef = @get('firebase').child("syncs/#{firebasePath}")
      newValue = newValue.toJSON() if newValue?.toJSON
      childRef.set(newValue)

    appSet = @set
    @set = ->
      keypathString = arguments[0]
      value = arguments[1]
      firstKeypathPart = keypathString.split(".")[0]
      if firstKeypathPart in(@_syncKeypaths || [])
        @_updateFirebaseChild(keypathString, value)
      appSet.apply(@, arguments)

Batman.App.classMixin(BatFire.AppMixin)

class BatFire.Storage extends Batman.StorageAdapter
  constructor: ->
    super
    firebaseClass = Batman.helpers.pluralize(@model.storageKey || @model.resourceName)
    @model.encode(@model.get('primaryKey'))

    _BatFireClearLoaded = @model.clear
    @model.clear = =>
      result = _BatFireClearLoaded.apply(@model)
      ref = @model.get('ref')
      ref?.off()
      @model.unset('ref')
      result

    @model.classAccessor 'firebasePath', ->
      children = ['records']
      if @get('isScopedToCurrentUser')
        uid = Batman.currentApp.get('currentUser.uid')
        if !uid?
          throw "#{firebaseClass} is scoped to currentUser -- you must be logged in to access it!"
        children.push('scoped')
        children.push(uid)
      children.push(firebaseClass)
      children.join("/")

    @model.accessor 'firebasePath', ->
      children = ['records']
      if @get('isScopedToCurrentUser')
        uid = if @get('isNew')
            Batman.currentApp.get('currentUser.uid')
          else
            @get('created_by_uid')
        if !uid?
          throw "#{firebaseClass} #{@get("id") || 'record'} is scoped to currentUser -- you must be logged in to access them!"
        children.push('scoped')
        children.push(uid)
      children.push(firebaseClass)

      if !@isNew()
        children.push(@get('id'))
      children.join("/")

    @model.encodesTimestamps = ->
      @accessor('_encodesTimestamps', -> true)
      @encode('created_at', 'updated_at', {
        encode: (value) -> value.toISOString()
        decode: (value) -> new Date(value)
        })

  _createRef: (env) ->
    try
      firebaseChildPath = env.subject.get('firebasePath')
      ref = Batman.currentApp.get('firebase').child(firebaseChildPath)
      if env.action is 'create'
        ref = ref.push()
    catch e
      env.error = e
    ref

  _listenToList: (ref, callback) ->
    return if @model.get('ref')
    ref.on 'child_added', (snapshot) =>
      record = @model.createFromJSON(snapshot.val())
    ref.on 'child_removed', (snapshot) =>
      record = @model.createFromJSON(snapshot.val())
      @model.get('loaded').remove(record)
    ref.on 'child_changed', (snapshot) =>
      record = @model.createFromJSON(snapshot.val())
    @model.set('ref', ref)


  @::before 'destroy', 'destroyAll', @skipIfError (env, next) ->
    if env.subject.get('hasUserOwnership')
      if env.action is 'destroyAll'
        env.error = new Error("You can't call destroyAll on these records because some may belong to other users.")
      if env.action is 'destroy' and !env.subject.get('isOwnedByCurrentUser')
        env.error = new Error("You can't destroy this record becasue it doesn't belong to you.")
    next()

  @::before 'create', 'update', 'read', 'destroy', 'readAll', 'destroyAll', @skipIfError (env, next) ->
    env.primaryKey = @model.primaryKey
    env.firebaseRef = @_createRef(env)
    next()

  @::after 'create', 'update', 'read', 'destroy', @skipIfError (env, next) ->
    env.result = env.subject
    next()

  create: @skipIfError (env, next) ->
    firebaseId = env.firebaseRef.name()
    env.subject._withoutDirtyTracking ->
      if env.subject.get('_encodesTimestamps')
        @set('created_at', new Date)
        @set('updated_at', new Date)
      if env.subject.get('_belongsToCurrentUser')
        for attr in BatFire.AuthModelMixin.CREATED_BY_FIELDS
          @set("created_by_#{attr}", Batman.currentApp.get('currentUser').get(attr))
      @set(env.primaryKey, firebaseId)
    env.firebaseRef.set env.subject.toJSON(), (err) ->
      if err
        env.error = err
      next()

  read: @skipIfError (env, next) ->
    env.firebaseRef.once 'value', (snapshot) =>
      data = snapshot.val()
      if !data?
        env.error = new @constructor.NotFoundError
      else
        env.subject._withoutDirtyTracking -> @fromJSON(data)
      next()

  update: @skipIfError (env, next) ->
    env.subject._withoutDirtyTracking ->
      if env.subject.get('_encodesTimestamps')
        @set('updated_at', new Date)
    env.firebaseRef.set env.subject.toJSON(), (err) ->
      if err
        env.error = err
      next()

  destroy: @skipIfError (env, next) ->
    env.firebaseRef.remove (err) ->
      if err
        env.error = err
      next()

  readAll: @skipIfError (env, next) ->
    @_listenToList(env.firebaseRef)
    env.firebaseRef.once 'value', (listSnapshot) =>
      listData = listSnapshot.val()
      env.result = (env.subject.createFromJSON(item) for id, item of listData)
      next()

  destroyAll: @skipIfError (env, next) ->
    env.firebaseRef.remove (err) ->
      if err
        env.error = err
      next()
class BatFire.User extends Batman.Object

BatFire.AuthAppMixin =
  initialize: ->
    @authorizesWithFirebase = (@providers...) ->
      @set 'currentUser', new BatFire.User
      @on 'run', =>
        @set 'auth', new FirebaseSimpleLogin @get('firebase.ref'), (err, user) =>
          if err?
            throw err
          else
            @_updateCurrentUser(user)

    @_updateCurrentUser = (attrs={}) ->
      @set("currentUser", new BatFire.User(attrs))

    @login = (provider, options={}) ->
      if @providers.length is 1
        provider ?= @providers[0]
      if (@providers.length) and (provider not in @providers)
        throw "Auth provider #{provider} not in whitelisted providers [#{@providers.join(", ")}]"

      @get('auth').login(provider, options)

    @logout = ->
      @get('auth').logout()
      Batman._scopedModels ?= []
      model.clear() for model in Batman._scopedModels
      @_updateCurrentUser({})

    @classAccessor 'loggedIn', ->  !!@get('currentUser.uid')
    @classAccessor 'loggedOut', -> !@get('loggedIn')

Batman.App.classMixin(BatFire.AuthAppMixin)


class BatFire.CurrentUserValidator extends Batman.Validator
  @triggers 'ownedByCurrentUser'

  validateEach: (errors, record, key, callback) ->
    if !record.get('isNew') # these are only set after create
      if !record.get('hasOwner')
        errors.add('base', "This record doesn't have an owner!")
      else if !record.get('isOwnedByCurrentUser')
        errors.add('base', "You don't own this record!")
    else
      if !Batman.currentApp.get('loggedIn')
        errors.add('base', "You must be logged in to create this!")
    callback()

Batman.Validators.push(BatFire.CurrentUserValidator)

BatFire.AuthModelMixin =
  CREATED_BY_FIELDS: ['uid', 'email', 'username']

  initialize: ->
    @belongsToCurrentUser = ({scoped, ownership}={})->
      for attr in BatFire.AuthModelMixin.CREATED_BY_FIELDS
        do (attr) =>
          accessorName = "created_by_#{attr}"
          @accessor(accessorName, Batman.Model.defaultAccessor) # uses default accessor
          @encode(accessorName)

      @accessor 'hasOwner', -> @get('created_by_uid')?
      @accessor 'isOwnedByCurrentUser', ->
        @get('hasOwner') and @get('created_by_uid') is Batman.currentApp.get('currentUser.uid')

      # used by the storage adapter:
      @set('_belongsToCurrentUser', true)
      @accessor('_belongsToCurrentUser', -> true)

      if ownership
        @validate('created_by_uid',{ownedByCurrentUser: true})
        @set('hasUserOwnership', true) # picked up in the storage adapter
        @accessor 'hasUserOwnership', -> true # picked up in the storage adapter
        @encode('hasUserOwnership', as: 'has_user_ownership')

      if scoped
        Batman._scopedModels ?= []
        Batman._scopedModels.push(@)
        @set('isScopedToCurrentUser', true) # picked up by storage adapter
        @accessor 'isScopedToCurrentUser', -> @constructor.get('isScopedToCurrentUser')



Batman.Model.classMixin(BatFire.AuthModelMixin)