# The nuki-reloaded plugin
module.exports = (env) ->

  Promise = env.require 'bluebird'
  nukiApi = require 'nuki-bridge-api'
  NukiObject = require 'nuki-bridge-api/lib/nuki'
  commons = require('pimatic-plugin-commons')(env)
  M = env.matcher
  _ = require('lodash')
  internalIp = require('internal-ip')

  # ###NukiPlugin class
  class NukiPlugin extends env.plugins.Plugin

    init: (app, @framework, @config) =>
      @debug = @config.debug || false
      @bridge = new nukiApi.Bridge @config.host, @config.port, @config.token
      env.logger.debug "New nukiApi.Bridge created"

      @callbackPort = @config.callbackPort ? 12321
      @restartTime = @config.restartTime ? 60000
      @nrOfRestarts = 10
      @bridgeReady = false

      @connectNuki = () =>
        internalIp.v4()
        .then (ip)=>
          @ip = ip
          env.logger.debug "Ip address for callback: " + @ip
          return @bridge.getCallbackUrls()
        .then (urls)=>
          env.logger.debug "Current callbacks: " + JSON.stringify(urls,null,2)
          if _.size(urls)>=3
            env.logger.info "Maximum number of callbacks reached on Nuki bridge"
            if @nrOfRestarts > 0
              env.logger.info "Please remove a callback on the bridge. The connection to the bridge will restart in " + @restartTime/1000 + " seconds. " + @nrOfRestarts + " restarts left."
              @nrOfRestarts -= 1
              @retryTimer = setTimeout(@connectNuki,@restartTime)
            else
              env.logger.info "Restarts unsuccesful. Please remove a callback on the bridge and restart the plugin"
              throw new Error("Maximum number of callbacks reached on Nuki bridge, Please remove a callback on the bridge and restart the plugin")
          else
            for cb in urls
              _url = @ip + ":" + @callbackPort
              if String(cb.url).indexOf(_url)>=0
                env.logger.debug "callback found, deleting"
                @bridge.removeCallback(cb.id)
            env.logger.info "Adding callback"
            return @bridge.addCallback(@ip, @callbackPort, true)
        .then (cbs)=>
          #env.logger.debug "Callback added " + JSON.stringify(cbs,null,2)
          cbs.on 'action', @stateHandler
          cbs.on 'action', @batteryCriticalHandler
        .finally ()=>
          @emit 'bridgeReady'
          @bridgeReady = true
          env.logger.debug "Callback on '#{@ip}' initialized"
        .catch (err)=>
          switch err.code
            when 'ECONNREFUSED'
              env.logger.info "Nuki bridge can't be reached"
              if @nrOfRestarts > 0
                env.logger.info "Please enable the bridge. The connection to the bridge will restart in " + @restartTime/1000 + " seconds. " + @nrOfRestarts + " restarts left."
                @nrOfRestarts -= 1
                @retryTimer = setTimeout(@connectNuki,@restartTime)
              else
                env.logger.info "Restarts unsuccesful. Please enable the bridge and restart the plugin"
            else
              env.logger.debug "Info initializing callback: " + err
          @bridgeReady = false

      @connectNuki()

      @base = commons.base @, 'Plugin'

      # register devices
      deviceConfigDef = require("./device-config-schema")
      @framework.deviceManager.registerDeviceClass("NukiDevice",
        configDef: deviceConfigDef.NukiDevice,
        createCallback: (@config, lastState) =>
          new NukiDevice(@config, @, lastState)
      )

      @framework.on "after init", =>
        # Check if the mobile-frontend was loaded and get a instance
        mobileFrontend = @framework.pluginManager.getPlugin 'mobile-frontend'
        if mobileFrontend?
          mobileFrontend.registerAssetFile 'js', 'pimatic-nuki-reloaded/ui/nuki.coffee'
          mobileFrontend.registerAssetFile 'css', 'pimatic-nuki-reloaded/ui/nuki.css'
          mobileFrontend.registerAssetFile 'html', 'pimatic-nuki-reloaded/ui/nuki.jade'
          #mobileFrontend.registerAssetFile 'js', 'pimatic-tado-reloaded/ui/vendor/spectrum.js'
          #mobileFrontend.registerAssetFile 'css', 'pimatic-tado-reloaded/ui/vendor/spectrum.css'
          #mobileFrontend.registerAssetFile 'js', 'pimatic-tado-reloaded/ui/vendor/async.js'
        else
          env.logger.warn 'your plugin could not find the mobile-frontend. No gui will be available'

      @framework.ruleManager.addActionProvider(new NukiActionProvider(@framework))

      # auto-discovery
      @framework.deviceManager.on('discover', (eventData) =>
        @framework.deviceManager.discoverMessage 'pimatic-nuki-reloaded', 'Searching for Nuki Smart Locks.'
        @lastId = null
        @bridge.list().then (nukiDevices) =>
          for device in nukiDevices
            do (device) =>
              @lastId = @base.generateDeviceId @framework, "nuki", @lastId
              if _.find(@framework.deviceManager.devicesConfig,(d) =>
                #env.logger.debug "Discover---->: " +d.nukiId+ ", = "+device.nukiId
                return d.nukiId is device.nukiId
                )
                env.logger.info "Device '" + device.nukiId + "' already in config"
              else
                deviceConfig =
                  id: @lastId
                  name: device.name
                  class: 'NukiDevice'
                  nukiId: String device.nukiId

                @framework.deviceManager.discoveredDevice(
                  'pimatic-nuki-reloaded',
                  "#{deviceConfig.name} (#{deviceConfig.nukiId})",
                  deviceConfig
                )
      )

    stateHandler: (state,response) =>
      resp =
        state: state
        response: response
      #env.logger.debug "emitting action " + JSON.stringify(resp,null,2)
      @emit 'action', resp

    batteryCriticalHandler: (batteryCritical) =>
      #env.logger.debug "emitting batteryCritical"
      @emit 'batteryCritical', batteryCritical


  class NukiDevice extends env.devices.Device

    template: 'nuki'

    actions:
      changeStateTo:
        params:
          state:
            type: "boolean"

    constructor: (@config, @plugin, lastState) ->
      @id = @config.id
      @name = @config.name

      @nukiId = @config.nukiId
      @ip = @plugin.ip
      @configInterval = @config.interval ? 21600

      env.logger.debug "Start constructor Nuki device"

      # attributes state is used for gui button
      @addAttribute('state',
        description: "State of the lock"
        type: "boolean"
        hidden: true
      )
      @addAttribute('lock',
        description: "Status of the lock"
        type: "string"
        acronym: "door"
        hidden: false
      )
      @addAttribute('status',
        description: "Status of the communicsation and lock"
        type: "string"
        acronym: "status"
      )
      @addAttribute('battery',
        description: "Critical status of the battery"
        type: "boolean"
        acronym: "battery"
        labels: ["critical","ok"]
      )

      # fixed attributes
      @_state = laststate?.state?.value ? false
      @_battery = laststate?.battery?.value ? false
      @_lock = laststate?.lock?.value ? ""
      @_status = laststate?.status?.value ? "ready"


      #creation of extra info attributes
      for info in @config.infos
        do (info) =>
          env.logger.debug "Info: " + JSON.stringify(info,null,2)
          @addAttribute(info.name ,
            description: info.name
            type: info.type ? "string"
            acronym: info.acronym ? info.name
            unit: info.unit ? ""
          )

          if info.type is "number"
            @[addUnderscore info.name] = laststate?[info.name]?.value ? 0
          else if @attributes[info.name].type is "boolean"
            @[addUnderscore info.name] = laststate?[info.name]?.value ? false
          else
            @[addUnderscore info.name] = laststate?[info.name]?.value ? ""

          env.logger.debug "@_[info.name] " + @[addUnderscore info.name]

          @_createGetter(info.name, =>
            return Promise.resolve @[addUnderscore info.name]
          )

      #initialization on restart of plugin/pimatic
      @plugin.on 'bridgeReady', @bridgeReadyHandler = ()=>

        env.logger.debug "BridgeReady received for " + @id

        @nuki = new NukiObject @plugin.bridge, @config.nukiId
        env.logger.debug "@nuki created for " + @id

        @plugin.on 'action', @stateHandler
        env.logger.debug "State handler created for " + @id
        @plugin.on 'batteryCritical', @batteryCriticalHandler
        env.logger.debug "BatteryCritical handler created for " + @id

        @_requestUpdate()

      #initialization on restart of device
      if @plugin.bridgeReady
        @nuki = new NukiObject @plugin.bridge, @config.nukiId
        env.logger.debug "@nuki created for " + @id

        @plugin.on 'action', @stateHandler
        env.logger.debug "State handler created for " + @id
        @plugin.on 'batteryCritical', @batteryCriticalHandler
        env.logger.debug "BatteryCritical handler created for " + @id

        @_requestUpdate()

      super()

    getTemplateName: -> "nuki"

    addUnderscore = (name)->
      return "_" + name

    batteryCriticalHandler: (battery, nukiId)=>
      unless nukiId is @nukiId then return
      @_setBattery(battery ? true)

    stateHandler: (_action)=>

      unless (Number _action.response.nukiId) == Number(@nukiId) then return

      state = _action.state
      lastKnownState = _action.response?.lastKnownState ? _action.response

      ###
      LockStateV1_2 =
        UNCALIBRATED: 0,
        LOCKED: 1,
        UNLOCKING: 2,
        UNLOCKED: 3,
        LOCKING: 4,
        UNLATCHED: 5,
        UNLOCKED_LOCK_N_GO: 6,
        UNLATCHING: 7,
        MOTOR_BLOCKED: 254,
        UNDEFINED: 255
      ###

      env.logger.debug "StateHandler, state received: " + state
      env.logger.debug "StateHandler, lastKnownState received: " + JSON.stringify(lastKnownState,null,2)


      switch state
        when nukiApi.lockState.LOCKED
          env.logger.debug "State LOCKED received for '#{@id}'"
          @_setState on
          @_setLock 'locked'
          @_setStatus 'received locked'
        when nukiApi.lockState.LOCKING
          env.logger.debug "State LOCKING received for '#{@id}'"
          @_setState on
          @_setLock 'locking'
          @_setStatus 'received locking'
        when nukiApi.lockState.UNLOCKING
          env.logger.debug "State UNLOCKING received for '#{@id}'"
          @_setState off
          @_setLock 'unlocking'
          @_setStatus 'received unlocking'
        when nukiApi.lockState.UNLOCKED
          env.logger.debug "State UNLOCKED received for '#{@id}'"
          @_setState off
          @_setLock 'unlocked'
          @_setStatus 'received unlocked'
        when nukiApi.lockState.UNLATCH
          env.logger.debug "State UNLATCH received for '#{@id}'"
          @_setState off
          @_setLock 'open'
          @_setStatus 'received open'
        when nukiApi.lockState.LOCK_N_GO
          env.logger.debug "State LOCK_N_GO received for '#{@id}'"
          @_setLock 'lock-n-go'
          @_setStatus 'received lock-n-go'
        when nukiApi.lockState.MOTOR_BLOCKED
          env.logger.debug "State MOTOR_BLOCKED received for '#{@id}'"
          @_setLock 'motor blocked'
          @_setStatus 'received motor blocked', false
        when nukiApi.lockState.UNDEFINED
          env.logger.debug "State UNDEFINED received for '#{@id}'"
          @_setLock 'undefined'
          @_setStatus 'received undefined', false
        else
          env.logger.debug "Unknown State received for '#{@id}', State nr: " + state
          @_setLock 'unknown'
          @_setStatus "received unknown: #{state}", false

      if lastKnownState?.batteryCritical?
        @_setBattery lastKnownState.batteryCritical

      #check if extra info values are received
      for info in @config.infos
        if lastKnownState?[info.name]?
          switch info.type
            when "number"
              @[addUnderscore info.name] = Number lastKnownState[info.name]
            when "boolean"
              @[addUnderscore info.name] = Boolean lastKnownState[info.name]
            else
              @[addUnderscore info.name] = lastKnownState[info.name]
          @emit info.name, @[addUnderscore info.name]

    actionHandler: (action)=>
      ###
      LockAction =
        UNLOCK: 1,
        LOCK: 2,
        UNLATCH: 3,
        LOCK_N_GO: 4,
        LOCK_N_GO_WITH_UNLATCH: 5
      ###

      env.logger.debug "ActionHandler, action received: " + JSON.stringify(action,null,2)

      switch action
        when nukiApi.lockAction.LOCK
          @nuki.lockAction(nukiApi.lockAction.LOCK, false) #nowait
          .then (resp)=>
            env.logger.debug "'#{@id}' locked"
            @_setState on
            @_setLock "locked"
          .catch (err) =>
            @_setStatus "set locked error " + err.statusCode 
            env.logger.debug "Error locking '#{@id}': " + JSON.stringify(err,null,2)
        when nukiApi.lockAction.UNLOCK
          @nuki.lockAction(nukiApi.lockAction.UNLOCK, false) #nowait
          .then (resp)=>
            env.logger.debug "'#{@id}' unlocked"
            @_setState off
            @_setLock "unlocked"
          .catch (err) =>
            env.logger.debug "Error unlocking '#{@id}': " + JSON.stringify(err,null,2)
        else
          env.logger.debug "Action '#{action}' not implemented"
      Promise.resolve()

    _requestUpdate: () =>
      env.logger.debug "Requesting update"

      @_setStatus "requesting update" 
      @plugin.bridge.list()
      .then (list) =>
        _nuki = _.find(list,(n)=>(Number n.nukiId) == (Number @nukiId))
        if _nuki?.lastKnownState?
          _state = _nuki.lastKnownState.state
          env.logger.debug "LockState is #{_state}"
          if typeof _state is "string"
            _state = parseInt _state
          @stateHandler {state: _state, response: _nuki}
      .catch (error) =>
        @_setStatus "requesting update error: " + error.statusCode, false
        env.logger.error "Error requesting update: " + (error.code ? error)
      .finally () =>
        @scheduleUpdate = setTimeout(@_requestUpdate, @configInterval * 1000)


    changeStateTo: (state) ->
      if state is @_state then return
      if Boolean state
        @_setStatus "set locked" 
        @actionHandler(nukiApi.lockAction.LOCK)
        .then ()=>
          @_setLock "locked"
          @_setState state
          return Promise.resolve()
        .catch (err)=>
          env.logger.debug "Error " + err
          @_setStatus "set locked error: " + err.statusCode, false
          return Promise.reject()
      else
        @_setStatus "set unlocked" 
        @actionHandler(nukiApi.lockAction.UNLOCK)
        .then ()=>
          @_setLock "unlocked"
          @_setState state
          return Promise.resolve()
        .catch (err)=>
          env.logger.debug "Error " + err
          @_setStatus "set unlocked error: " + err.statusCode, false
          return Promise.reject()

    getState: () -> Promise.resolve @_state
    _setState: (state) =>
      @_state = state
      @emit 'state', state

    getBattery: () -> Promise.resolve @_battery
    _setBattery: (battery) =>
      @_battery = battery
      @emit 'battery', battery

    getLock: () -> Promise.resolve @_lock
    _setLock: (status) =>
      @_lock = status
      @emit 'lock', status

    getStatus: () -> Promise.resolve @_status
    _setStatus: (status, reset=true) =>
      @_status = status
      @emit 'status', status
      # clear running readyTimer
      clearTimeout(@readyTimer) if @readyTimer?
      # reset status
      if reset
        @readyTimer = setTimeout(=>
          @_status = "ready"
          @emit "status", "ready"
        ,3000)

    execute: (command, options) =>
      return new Promise((resolve,reject) =>

        env.logger.debug "Execute command: " + command + ", options: " + JSON.stringify(options,null,2)
        switch command
          when "lock"
            @_setStatus "set locked" 
            env.logger.debug "Lock Nuki #{@id}"
            @nuki.lockAction(nukiApi.lockAction.LOCK, false) #nowait
            .then (resp)=>
              env.logger.debug "Nuki locked"
              @_setState on
              @_setLock "locked"
              resolve()
            .catch (err) =>
              env.logger.debug "Error locking #{@id}: " + JSON.stringify(err,null,2)
              @_setStatus "set locked error "+err.statusCode, false
              reject()
          when "unlock"
            env.logger.debug "Unlock Nuki #{@id}"
            @_setStatus "set unlocked" 
            @nuki.lockAction(nukiApi.lockAction.UNLOCK, false) #nowait
            .then (resp)=>
              env.logger.debug "Nuki #{@id} unlocked"
              @_setState off
              @_setLock "unlocked"
              resolve()
            .catch (err) =>
              env.logger.debug "Error unlocking #{@id}: " + JSON.stringify(err,null,2)
              @_setStatus "set unlocked error "+err.statusCode, false
              reject()
          else
            env.logger.debug "Command not implemented: " + command
            reject()
        resolve()
      )

    destroy: () ->
      clearTimeout(@scheduleUpdate)
      @plugin.removeListener 'action', @stateHandler
      @plugin.removeListener 'batteryCritical', @batteryCriticalHandler
      @plugin.removeListener 'bridgeReady', @bridgeReadyHandler

      super()

  class NukiActionProvider extends env.actions.ActionProvider

    constructor: (@framework) ->

    parseAction: (input, context) =>

      nukiDevice = null
      @options = {}

      nukiDevices = _(@framework.deviceManager.devices).values().filter(
        (device) => device.config.class == "NukiDevice"
      ).value()

      setCommand = (command) =>
        @command = command

      m = M(input, context)
        .match('nuki ')
        .matchDevice(nukiDevices, (m, d) ->
          # Already had a match with another device?
          if nukiDevice? and nukiDevices.id isnt d.id
            context?.addError(""""#{input.trim()}" is ambiguous.""")
            return
          nukiDevice = d
        )
        .or([
          ((m) =>
            return m.match(' lock', (m) =>
              setCommand('lock')
              match = m.getFullMatch()
            )
          ),
          ((m) =>
            return m.match(' unlock', (m) =>
              setCommand('unlock')
              match = m.getFullMatch()
            )
          )
        ])

      match = m.getFullMatch()
      if match? #m.hadMatch()
        env.logger.debug "Rule matched: '", match, "' and passed to Action handler"
        return {
          token: match
          nextInput: input.substring(match.length)
          actionHandler: new NukiActionHandler(@framework, nukiDevice, @command, @options)
        }
      else
        return null


  class NukiActionHandler extends env.actions.ActionHandler

    constructor: (@framework, @nukiDevice, @command, @options) ->

    executeAction: (simulate) =>
      if simulate
        return __("would have Nuki action \"%s\"", "")
      else
        @nukiDevice.execute(@command, @options)
        .then(()=>
          return __("\"%s\" Rule executed", @command)
        ).catch((err)=>
          return __("\"%s\" Rule not executed", "")
        )


  # ###Finally
  # Create a instance of NukiPlugin
  # and return it to the framework.
  return new NukiPlugin
