module.exports = (env) ->

  # ##Dependencies
  # * from node.js
  util = require 'util'
  
  # * pimatic imports.
  Promise = env.require 'bluebird'
  assert = env.require 'cassert'
  _ = env.require 'lodash'
  M = env.matcher
  Tail = env.Tail or require('tail').Tail
  t = env.require('decl-api').types
  LineByLineReader = require("line-by-line")

  # ##The LogReaderPlugin
  class LogReaderPlugin extends env.plugins.Plugin

    init: (app, @framework, @config) ->
      @framework.ruleManager.addPredicateProvider new LogWatcherPredicateProvider(@framework)

      deviceConfigDef = require("./device-config-schema")

      @framework.deviceManager.registerDeviceClass("LogWatcher", {
        configDef: deviceConfigDef.LogWatcher, 
        createCallback: (config, lastState) => return new LogWatcher(config, lastState)
      })

  plugin = new LogReaderPlugin

    # ##LogWatcher Sensor
  class LogWatcher extends env.devices.Sensor

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

      if @config.template?
        @template = @config.template

      @attributeValue = {}
      @changedAttributeValue = {}

      @attributes = {}
      # initialise all attributes
      for attr, i in @config.attributes
        do (attr) =>
          # legacy support
          if typeof attr is "string"
            attr = {
              name: attr
              type: "string"
            }
            @config.attributes[i] = attr

          name = attr.name
          assert attr.name?
          assert attr.type?

          lastValue = lastState?[name]?.value
          unless typeof lastValue is attr.type
            lastValue = null

          switch attr.type
            when "string"
              # that the value to 'unknown'
              @attributeValue[name] = lastValue
              # Get all possible values
              possibleValues = _.map(_.filter(@config.lines, (l) => l[name]?), (l) => l[name])
              # Add attribute definition
              @attributes[name] =
                description: name
                type: t.string
                enum: possibleValues
            when "number"
              @attributeValue[name] = lastValue
              @attributes[name] =
                description: name
                type: t.number
              @attributes[name].unit = attr.unit if attr.unit?
            when "boolean"
              @attributeValue[name] = lastValue
              @attributes[name] =
                description: name
                type: t.boolean
            else
              throw new Error("Illegal type: #{attr.type} for attributes #{name} in LogWatcher.")
          for property in ['label', 'acronym', 'discrete']
            @attributes[name][property] = attr[property] if attr[property]?
          if _.isArray attr.labels
            @attributes[name].labels = attr.labels
          # Create a getter for this attribute
          @_createGetter name, ( => Promise.resolve @attributeValue[name] )


      onLine = (data) =>
        # check all lines in config
        for line in @config.lines
          # for a match.
          matches = new RegExp(line.match).exec(data)
          if matches?
            # If a match occures then emit a "match"-event.
            @emit 'match', line, data, matches
        return

      @_tailing = no
      onMatch = (line, data, matches) =>
        # then check for each prop in the config
        for attr in @config.attributes
          # if the attr is registered for the log line.
          if attr.name of line
            # When a value for the attr is defined, then set the value
            # and emit the event.
            valueToSet = line[attr.name]
            value = null
            
            if attr.type is "boolean" 
              value = line[attr.name]
            else
              matchesRegexValue = valueToSet.match(/\$(\d+)/)
              if matchesRegexValue?
                value = matches[parseInt(matchesRegexValue[1], 10)]
              else 
                value = line[attr.name]

              if attr.type is "number" then value = parseFloat(value)

            if @_tailing
              @attributeValue[attr.name] = value
              @emit(attr.name, value)
            else
              if @attributeValue[attr.name] isnt value
                @attributeValue[attr.name] = value
                @changedAttributeValue[attr.name] = value
        return

      # When a match event occurs
      @on 'match', onMatch

      # read the file to get initial values:
      retryTimeout = 0
      reader = () =>
        retryTimeout += 10000 if retryTimeout <= 60000

        @lr.removeAllListeners() if @lr?
        @lr = new LineByLineReader(@config.file)

        @lr.on "error", (err) ->
          env.logger.error err.message
          env.logger.debug err.stack
          setTimeout reader, retryTimeout

        @lr.on "open", => retryTimeout = 0
        @lr.on "line", onLine
        @lr.on "end", =>
          @_tailing = yes
          for attrName, value of @changedAttributeValue
            @emit(attrName, value)
          @changedAttributeValue = {}
          # If we have read the full file then tail the file
          @tail = new Tail(@config.file)
          # On every new line in the log file
          @tail.on 'line', onLine
          @tail.on 'error', (errMessage) =>
            # Tail passes an error message string
            errMessage = errMessage.replace ': undefined', ''
            if @tail?
              @tail.unwatch()
              @tail.removeAllListeners()
            @lr.emit "error", new Error(errMessage)

      reader()
      super()

    destroy: () ->
      @removeAllListeners 'match'
      @lr.close() if @lr?
      @tail.unwatch() if @tail?
      super()

  class LogWatcherPredicateProvider extends env.predicates.PredicateProvider
    listener: []

    constructor: (@framework) ->

    parsePredicate: (input, context) ->
      for id, d of @framework.deviceManager.devices
        if d instanceof LogWatcher
          info = @_getLineWithPredicate d.config, input, context
          if info?
            return {
              token: info.token
              nextInput: input.substring(info.token.length)
              predicateHandler: new LogWatcherPredicateHandler(this, d, info.line)
            }
      return null

    _getLineWithPredicate: (config, input, context) ->
      for line in config.lines
        if line.predicate? 
          m = M(input, context).match(line.predicate)
          if m.hadMatch()
            match = m.getFullMatch()
            return {line, token: match, nextInput: input.substring(match.length)}
      return null

  class LogWatcherPredicateHandler extends env.predicates.PredicateHandler

    constructor: (@provider, @device, @line) ->

    setup: ->
      @deviceListener = (line, data) => 
        if @device._tailing and line.match is @line.match then @emit('change', 'event')
      @device.addListener 'match', @deviceListener
      super()
    getValue: -> Promise.resolve(false)
    destroy: -> 
      @device.removeListener 'match', @deviceListener
      super()
    getType: -> 'event'

  # For testing...
  @LogReaderPlugin = LogReaderPlugin
  @LogWatcherPredicateProvider = LogWatcherPredicateProvider
  # Export the plugin.
  return plugin