###
Predicate Provider
=================
A Predicate Provider provides a predicate for the Rule System. For predicate and rule explanations
take a look at the [rules file](rules.html). A predicate is a string that describes a state. A
predicate is either true or false at a given time. There are special predicates, 
called event-predicates, that represent events. These predicate are just true in the moment a 
special event happen.
###

__ = require("i18n-pimatic").__
Promise = require 'bluebird'
S = require 'string'
assert = require 'cassert'
_ = require 'lodash'
M = require './matcher'
types = require('decl-api').types

module.exports = (env) ->

  ###
  The Predicate Provider
  ----------------
  This is the base class for all predicate provider. 
  ###
  class PredicateProvider

    parsePredicate: (input, context) -> throw new Error("You must implement parsePredicate")


  class PredicateHandler extends require('events').EventEmitter

    getType: -> throw new Error("You must implement getType")
    getValue: -> throw new Error("You must implement getValue")

    setup: -> 
      # You must overwrite this method and set up your listener here.
      # You should call super() after that.
      if @_setupCalled then throw new Error("Setup already called!")
      @_setupCalled = yes
    destroy: -> 
      # You must overwrite this method and remove your listener here.
      # You should call super() after that.
      unless @_setupCalled then throw new Error("Destroy called, but setup was not called!")
      delete @_setupCalled
      @emit "destroy"
      @removeAllListeners()

    dependOnDevice: (device) ->
      recreateEmitter = (=> @emit "recreate")
      device.on "changed", recreateEmitter
      device.on "destroyed", recreateEmitter
      @on 'destroy', =>
        device.removeListener "changed", recreateEmitter
        device.removeListener "destroyed", recreateEmitter

    dependOnVariable: (variableManager, varName) ->
      recreateEmitter = ( (variable) => 
        if variable.name isnt varName
          return
        @emit "recreate"
      )
      variableManager.on "variableRemoved", recreateEmitter
      @on 'destroy', =>
        variableManager.removeListener "variableRemoved", recreateEmitter
  ###
  The Switch Predicate Provider
  ----------------
  Provides predicates for the state of switch devices like:

  * _device_ is on|off
  * _device_ is switched on|off
  * _device_ is turned on|off

  ####
  class SwitchPredicateProvider extends PredicateProvider

    presets: [
      {
        name: "switch turned on/off"
        input: "{device} is turned on"
      }
    ]

    constructor: (@framework) ->

    # ### parsePredicate()
    parsePredicate: (input, context) ->  

      switchDevices = _(@framework.deviceManager.devices).values()
        .filter((device) => device.hasAttribute( 'state')).value()

      device = null
      state = null
      match = null

      stateAcFilter = (v) => v.trim() isnt 'is switched' 
      M(input, context)
        .matchDevice(switchDevices, (next, d) =>
          next.match([' is', ' is turned', ' is switched'], acFilter: stateAcFilter, type: 'static')
            .match([' on', ' off'], param: 'state', type: 'select', (next, s) =>
              # Already had a match with another device?
              if device? and device.id isnt d.id
                context?.addError(""""#{input.trim()}" is ambiguous.""")
                return
              assert d?
              assert s in [' on', ' off']
              device = d
              state = s.trim() is 'on'
              match = next.getFullMatch()
          )
        )
 
      # If we have a match
      if match?
        assert device?
        assert state?
        assert typeof match is "string"
        # and state as boolean.
        return {
          token: match
          nextInput: input.substring(match.length)
          predicateHandler: new SwitchPredicateHandler(device, state)
        }
      else
        return null

  class SwitchPredicateHandler extends PredicateHandler

    constructor: (@device, @state) ->
      @dependOnDevice(@device)
    setup: ->
      @stateListener = (s) => @emit 'change', (s is @state)
      @device.on 'state', @stateListener
      super()
    getValue: -> @device.getUpdatedAttributeValue('state').then( (s) => (s is @state) )
    destroy: -> 
      @device.removeListener "state", @stateListener
      super()
    getType: -> 'state'


  ###
  The Presence Predicate Provider
  ----------------
  Handles predicates of presence devices like

  * _device_ is present
  * _device_ is not present
  * _device_ is absent
  ####
  class PresencePredicateProvider extends PredicateProvider

    presets: [
      {
        name: "device is present/absent"
        input: "{device} is present"
      }
    ]

    constructor: (@framework) ->

    parsePredicate: (input, context) ->

      presenceDevices = _(@framework.deviceManager.devices).values()
        .filter((device) => device.hasAttribute( 'presence')).value()

      device = null
      negated = null
      match = null

      stateAcFilter = (v) => v.trim() isnt 'not present'

      M(input, context)
        .matchDevice(presenceDevices, (next, d) =>
          next.match([' is', ' reports', ' signals'], type: "static")
            .match(
              [' present', ' absent', ' not present'], 
              acFilter: stateAcFilter, type: "select", param: "state", 
              (m, s) =>
                # Already had a match with another device?
                if device? and device.id isnt d.id
                  context?.addError(""""#{input.trim()}" is ambiguous.""")
                  return
                device = d
                negated = (s.trim() isnt "present") 
                match = m.getFullMatch()
            )
      )
      
      if match?
        assert device?
        assert negated?
        assert typeof match is "string"
        return {
          token: match
          nextInput: input.substring(match.length)
          predicateHandler: new PresencePredicateHandler(device, negated)
        }
      else
        return null

  class PresencePredicateHandler extends PredicateHandler

    constructor: (@device, @negated) ->
      @dependOnDevice(@device)
    setup: ->
      @presenceListener = (p) => 
        @emit 'change', (if @negated then not p else p)
      @device.on 'presence', @presenceListener
      super()
    getValue: -> 
      return @device.getUpdatedAttributeValue('presence').then( 
        (p) => (if @negated then not p else p)
      )
    destroy: -> 
      @device.removeListener "presence", @presenceListener
      super()
    getType: -> 'state'

  ###
  The Contact Predicate Provider
  ----------------
  Handles predicates of contact devices like

  * _device_ is opened
  * _device_ is closed
  ####
  class ContactPredicateProvider extends PredicateProvider

    presets: [
      {
        name: "device is opened/closed"
        input: "{device} is opened"
      }
    ]

    constructor: (@framework) ->

    parsePredicate: (input, context) ->

      contactDevices = _(@framework.deviceManager.devices).values()
        .filter((device) => device.hasAttribute( 'contact')).value()

      device = null
      negated = null
      match = null

      contactAcFilter = (v) => v.trim() in ['opened', 'closed']

      M(input, context)
        .matchDevice(contactDevices, (next, d) =>
          next.match(' is', type: "static")
            .match(
              [' open', ' close', ' opened', ' closed'], 
              acFilter: contactAcFilter, type: "select", 
              (m, s) =>
                # Already had a match with another device?
                if device? and device.id isnt d.id
                  context?.addError(""""#{input.trim()}" is ambiguous.""")
                  return
                device = d
                negated = (s.trim() in ["opened", 'open']) 
                match = m.getFullMatch()
            )
      )
      
      if match?
        assert device?
        assert negated?
        assert typeof match is "string"
        return {
          token: match
          nextInput: input.substring(match.length),
          predicateHandler: new ContactPredicateHandler(device, negated)
        }
      else
        return null

  class ContactPredicateHandler extends PredicateHandler

    constructor: (@device, @negated) ->
      @dependOnDevice(@device)
    setup: ->
      @contactListener = (p) => 
        @emit 'change', (if @negated then not p else p)
      @device.on 'contact', @contactListener
      super()
    getValue: -> @device.getUpdatedAttributeValue('contact').then(
      (p) => (if @negated then not p else p)
    )
    destroy: -> 
      @device.removeListener "contact", @contactListener
      super()
    getType: -> 'state'


  ###
  The Device-Attribute Predicate Provider
  ----------------
  Handles predicates for comparing device attributes like sensor values or other states:

  * _attribute_ of _device_ is equal to _value_
  * _attribute_ of _device_ equals _value_
  * _attribute_ of _device_ is not _value_
  * _attribute_ of _device_ is less than _value_
  * _attribute_ of _device_ is lower than _value_
  * _attribute_ of _device_ is greater than _value_
  * _attribute_ of _device_ is higher than _value_
  ####
  class DeviceAttributePredicateProvider extends PredicateProvider
    
    presets: [
      {
        name: "attribute of a device"
        input: "{attribute} of {device} is equal to {value}"
      }
    ]

    constructor: (@framework) ->

    # ### parsePredicate()
    parsePredicate: (input, context) ->

      allAttributes = _(@framework.deviceManager.getDevices())
        .map((device) => _.keys(device.attributes))
        .flatten().uniq().value()

      result = null
      matches = []

      M(input, context)
      .match(
        allAttributes,
        param: "attribute", wildcard: "{attribute}"
        (m, attr) =>
          info = {
            device: null
            attributeName: null
            comparator: null
            referenceValue: null
          }
          info.attributeName = attr
          devices = _(@framework.deviceManager.devices).values()
            .filter((device) => device.hasAttribute(attr)).value()

          m.match(' of ').matchDevice(devices, (next, device) =>
            info.device = device
            unless device.hasAttribute(attr) then return
            attribute = device.attributes[attr]
            setComparator =  (m, c) => info.comparator = c
            setRefValue = (m, v) => info.referenceValue = v
            end =  => matchCount++

            if attribute.type is types.boolean
              m = next.matchComparator('boolean', setComparator)
                .match(attribute.labels, wildcard: '{value}', (m, v) =>
                  if v is attribute.labels[0] then setRefValue(m, true)
                  else if v is attribute.labels[1] then setRefValue(m, false)
                  else assert(false)
              )
            else if attribute.type is types.number
              m = next.matchComparator('number', setComparator)
                .matchNumber(wildcard: '{value}', (m,v) => setRefValue(m, parseFloat(v)) )
              if attribute.unit? and attribute.unit.length > 0 
                possibleUnits = _.uniq([
                  " #{attribute.unit}", 
                  "#{attribute.unit}", 
                  "#{attribute.unit.toLowerCase()}", 
                  " #{attribute.unit.toLowerCase()}",
                  "#{attribute.unit.replace('°', '')}", 
                  " #{attribute.unit.replace('°', '')}",
                  "#{attribute.unit.toLowerCase().replace('°', '')}", 
                  " #{attribute.unit.toLowerCase().replace('°', '')}",
                  ])
                autocompleteFilter = (v) => v is " #{attribute.unit}"
                m = m.match(possibleUnits, {optional: yes, acFilter: autocompleteFilter})
            else if attribute.type is types.string
              m = next.matchComparator('string', setComparator)
                .or([
                  ( (m) => m.matchString(wildcard: '{value}', setRefValue) ),
                  ( (m) => 
                    if attribute.enum?
                      m.match(attribute.enum, wildcard: '{value}', setRefValue) 
                    else M(null) 
                  )
                ])
            if m.hadMatch()
              matches.push m.getFullMatch()
              if result?
                if result.device.id isnt info.device.id or 
                result.attributeName isnt info.attributeName
                  context?.addError(""""#{input.trim()}" is ambiguous.""")
              result = info
          )
      )

      if result?
        assert result.device?
        assert result.attributeName?
        assert result.comparator?
        assert result.referenceValue?
        # take the longest match
        match = _(matches).sortBy( (s) => s.length ).last()
        assert typeof match is "string" 

        return {
          token: match
          nextInput: input.substring(match.length)
          predicateHandler: new DeviceAttributePredicateHandler(
            result.device, result.attributeName, result.comparator, result.referenceValue
          )
        }
        
      return null


  class DeviceAttributePredicateHandler extends PredicateHandler

    constructor: (@device, @attribute, @comparator, @referenceValue) ->
      @dependOnDevice(@device)

    setup: ->
      lastState = null
      @attributeListener = (value) =>
        state = @_compareValues(@comparator, value, @referenceValue)
        if state isnt lastState
          lastState = state
          @emit 'change', state
      @device.on @attribute, @attributeListener
      super()
    getValue: -> 
      @device.getUpdatedAttributeValue(@attribute).then( (value) =>
        @_compareValues(@comparator, value, @referenceValue)
      )
    destroy: -> 
      @device.removeListener @attribute, @attributeListener
      super()
    getType: -> 'state'

    # ### _compareValues()
    ###
    Does the comparison.
    ###
    _compareValues: (comparator, value, referenceValue) ->
      if typeof referenceValue is "number"
        value = parseFloat(value)
      result = switch comparator
        when '==' then value is referenceValue
        when '!=' then value isnt referenceValue
        when '<' then value < referenceValue
        when '>' then value > referenceValue
        when '<=' then value <= referenceValue
        when '>=' then value >= referenceValue
        else throw new Error "Unknown comparator: #{comparator}"
      return result


  ###
  The Device-Attribute Watchdog Provider
  ----------------
  Handles predicates that will become true if a attribute of a device was not updated for a
  certain time.

  * _attribute_ of _device_ was not updated for _time_
  ####
  class DeviceAttributeWatchdogProvider extends PredicateProvider

    presets: [
      {
        name: "attribute of a device not updated"
        input: "{attribute} of {device} was not updated for {duration} minutes"
      }
    ]

    constructor: (@framework) ->

    # ### parsePredicate()
    parsePredicate: (input, context) ->

      allAttributes = _(@framework.deviceManager.getDevices())
        .map((device) => _.keys(device.attributes))
        .flatten().uniq().value()

      result = null
      match = null

      M(input, context)
      .match(allAttributes, wildcard: "{attribute}", type: "select", (m, attr) =>
        info = {
          device: null
          attributeName: null
          timeMs: null
        }

        info.attributeName = attr
        devices = _(@framework.deviceManager.devices).values()
          .filter( (device) => device.hasAttribute(attr) ).value()
        m.match(' of ').matchDevice(devices, (m, device) =>
          info.device = device
          unless device.hasAttribute(attr) then return
          attribute = device.attributes[attr]

          m.match(' was not updated for ', type: "static")
            .matchTimeDuration(wildcard: "{duration}", type: "text", (m, {time, unit, timeMs}) =>
              info.timeMs = timeMs
              result = info
              match = m.getFullMatch()
            )
        )
      )

      if result?
        assert result.device?
        assert result.attributeName?
        assert result.timeMs?

        return {
          token: match
          nextInput: input.substring(match.length)
          predicateHandler: new DeviceAttributeWatchdogPredicateHandler(
            result.device, result.attributeName, result.timeMs
          )
        }
        
      return null


  class DeviceAttributeWatchdogPredicateHandler extends PredicateHandler

    constructor: (@device, @attribute, @timeMs) ->
      @dependOnDevice(@device)
    setup: ->
      @_state = false
      @_rescheduleTimeout()
      @attributeListener = ( => 
        if @_state is true
          @_state = false
          @emit 'change', false
        @_rescheduleTimeout() 
      )
      @device.on @attribute, @attributeListener
      super()
    getValue: -> Promise.resolve(@_state)
    destroy: -> 
      @device.removeListener @attribute, @attributeListener
      clearTimeout(@_timer)
      super()
    getType: -> 'state'

    _rescheduleTimeout: ->
      clearTimeout(@_timer)
      @_timer = setTimeout( ( =>
        @_state = true
        @emit 'change', true 
      ), @timeMs)

  ###
  The Variable Predicate Provider
  ----------------
  Handles comparison of variables
  ####
  class VariablePredicateProvider extends PredicateProvider

    presets: [
        {
          name: "Variable comparison"
          input: "{expr} = {expr}"
        }
      ]

    constructor: (@framework) ->

    parsePredicate: (input, context) ->
      result = null

      M(input, context)
        .matchAnyExpression( (next, leftTokens) =>
          next.matchComparator('number', (next, comparator) =>
            next.matchAnyExpression( (next, rightTokens) =>
              result = {
                leftTokens
                rightTokens
                comparator
                match: next.getFullMatch()
              }
            )
          )
        )
      
      if result?
        assert Array.isArray result.leftTokens
        assert Array.isArray result.rightTokens
        assert result.comparator in ['==', '!=', '<', '>', '<=', '>=']
        assert typeof result.match is "string"

        variables = @framework.variableManager.extractVariables(
          result.leftTokens.concat result.rightTokens
        )
        for v in variables?
          unless @framework.variableManager.isVariableDefined(v)
            context.addError("Variable $#{v} is not defined.")
            return null

        return {
          token: result.match
          nextInput: input.substring(result.match.length)
          predicateHandler: new VariablePredicateHandler(
            @framework, result.leftTokens, result.rightTokens, result.comparator
          )
        }
      else
        return null

  class VariablePredicateHandler extends PredicateHandler

    constructor: (@framework, @leftTokens, @rightTokens, @comparator) ->

    setup: ->
      @lastState = null
      @variables = @framework.variableManager.extractVariables(
        @leftTokens.concat @rightTokens
      )
      for variable in @variables
        @dependOnVariable(@framework.variableManager, variable)
      @changeListener = (variable, value) =>
        unless variable.name in @variables then return
        evalPromise = @_evaluate()
        evalPromise.then( (state) =>
          if state isnt @lastState
            @lastState = state
            @emit 'change', state
        ).catch( (error) =>
          env.logger.error "Error in VariablePredicateHandler:", error.message
          env.logger.debug error
        )
      
      @framework.variableManager.on("variableValueChanged", @changeListener)
      super()
    getValue: -> 
      return @_evaluate()
    destroy: -> 
      @framework.variableManager.removeListener("variableValueChanged", @changeListener)
      super()
    getType: -> 'state'

    _evaluate: ->
      leftPromise = @framework.variableManager.evaluateExpression(@leftTokens)
      rightPromise = @framework.variableManager.evaluateExpression(@rightTokens)
      return Promise.all([leftPromise, rightPromise]).then( ([leftValue, rightValue]) =>
        return state = @_compareValues(leftValue, rightValue)
      )

    # ### _compareValues()
    ###
    Does the comparison.
    ###
    _compareValues: (left, right) ->
      if @comparator in ["<", ">", "<=", ">="]
        if typeof left is "string"
          if isNaN(left)
            throw new Error("Can not compare strings with #{@comparator}!")
          left = parseFloat(left)
        if typeof right is "string"
          if isNaN(right)
            throw new Error("Can not compare strings with #{@comparator}!")
          right = parseFloat(right)

      return switch @comparator
        when '==' then left is right
        when '!=' then left isnt right
        when '<' then left < right
        when '>' then left > right
        when '<=' then left <= right
        when '>=' then left >= right
        else throw new Error "Unknown comparator: #{@comparator}"


  class VariableUpdatedPredicateProvider extends PredicateProvider

    presets: [
        {
          name: "Variable changes"
          input: "{variable} changes"
        }
        {
          name: "Variable increased/decreased"
          input: "{variable} increased"
        }
      ]

    constructor: (@framework) ->

    parsePredicate: (input, context) ->
      variableName = null
      mode = null

      setVariableName = (next, name) => variableName = name.substring(1)
      setMode = (next, match) => mode = match.trim()

      m = M(input, context)
        .matchVariable(setVariableName)
        .match([
          " changes", " gets updated", 
          " increased", " decreased", 
          " is increasing", " is decreasing"
        ], setMode)

      if m.hadMatch()
        match = m.getFullMatch()
        assert typeof variableName is "string"
        assert mode?
        return {
          token: match
          nextInput: input.substring(match.length)
          predicateHandler: new VariableUpdatedPredicateHandler(
            @framework, variableName, mode
          )
        }
      else
        return null

  class VariableUpdatedPredicateHandler extends PredicateHandler

    constructor: (@framework, @variableName, @mode) ->

    setup: ->
      @lastValue = null
      @state = false
      @dependOnVariable(@framework.variableManager, @variableName)
      @changeListener = (variable, value) =>
        unless variable.name is @variableName then return
        switch @mode
          when 'changes'
            if @lastValue isnt value
              @emit 'change', "event"
          when 'gets updated'
            @emit 'change', "event"
          when 'increased'
            if value > @lastValue
              @emit 'change', "event"
          when 'decreased'
            if value < @lastValue
              @emit 'change', "event"
          when 'is increasing'
            if value > @lastValue
              if not @state
                @state = true
                @emit 'change', true
            else
              if @state
                @state = false
                @emit 'change', false
          when 'is decreasing'
            if value < @lastValue
              if not @state
                @state = true
                @emit 'change', true
            else
              if @state
                @state = false
                @emit 'change', false
        @lastValue = value
       
      @framework.variableManager.on("variableValueChanged", @changeListener)
      super()
    getValue: -> Promise.resolve(@state)
    destroy: ->
      @framework.variableManager.removeListener("variableValueChanged", @changeListener)
      super()
    getType: -> 
      switch @mode
        when 'is increasing', 'is decreasing' then return 'state'
        else return 'event'

  class ButtonPredicateProvider extends PredicateProvider

    presets: [
        {
          name: "Button pressed"
          input: "{button} is pressed"
        }
      ]

    constructor: (@framework) ->

    parsePredicate: (input, context) ->

      matchCount = 0
      matchingDevice = null
      matchingButtonId = null
      end = () => matchCount++
      onButtonMatch = (m, {device, buttonId}) =>
        matchingDevice = device
        matchingButtonId = buttonId

      buttonsWithId = [] 

      for id, d of @framework.deviceManager.devices
        continue unless d instanceof env.devices.ButtonsDevice
        for b in d.config.buttons
          buttonsWithId.push [{device: d, buttonId: b.id}, b.id]
          buttonsWithId.push [{device: d, buttonId: b.id}, b.text] if b.id isnt b.text

      m = M(input, context)
        .match('the ', optional: true)
        .match(
          buttonsWithId, 
          wildcard: "{button}"
          onButtonMatch
        )
        .match(' button', optional: true)
        .match(' is', optional: true)
        .match(' pressed')

      if m.hadMatch()
        match = m.getFullMatch()
        return {
          token: match
          nextInput: input.substring(match.length)
          predicateHandler: new ButtonPredicateHandler(this, matchingDevice, matchingButtonId)
        }
      return null

  class ButtonPredicateHandler extends PredicateHandler

    constructor: (@provider, @device, @buttonId) ->
      assert @device? and @device instanceof env.devices.ButtonsDevice
      assert @buttonId? and typeof @buttonId is "string"
      @dependOnDevice(@device)
      
    setup: ->
      @buttonPressedListener = ( (id) =>
        if id is @buttonId
          @emit 'change', 'event'
      )
      @device.on 'button', @buttonPressedListener
      super()

    getValue: -> Promise.resolve(false)
    destroy: -> 
      @device.removeListener 'button', @buttonPressedListener
      super()
    getType: -> 'event'


  class StartupPredicateProvider extends PredicateProvider

    presets: [
        {
          name: "pimatic is starting"
          input: "pimatic is starting"
        }
      ]

    constructor: (@framework) ->

    parsePredicate: (input, context) ->
      m = M(input, context).match(["pimatic is starting"])

      if m.hadMatch()
        match = m.getFullMatch()
        return {
          token: match
          nextInput: input.substring(match.length)
          predicateHandler: new StartupPredicateHandler(@framework)
        }
      else
        return null

  class StartupPredicateHandler extends PredicateHandler

    constructor: (@framework) ->

    setup: ->
      @framework.once "after init", =>
        @emit 'change', "event"
      super()
    getValue: -> Promise.resolve(false)
    getType: -> 'event'

  return exports = {
    PredicateProvider
    PredicateHandler
    PresencePredicateProvider
    SwitchPredicateProvider
    DeviceAttributePredicateProvider
    VariablePredicateProvider
    VariableUpdatedPredicateProvider
    ContactPredicateProvider
    ButtonPredicateProvider
    DeviceAttributeWatchdogProvider
    StartupPredicateProvider
  }
