#
# Copyright (c) 2018 ITO SOFT DESIGN Inc.
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

<<-DOC
Here is a sample configuration.
Puts your configuration to config/plugins/plc_mapper.yml

plcs:
  - description: Machine A-123
    protocol: mc_protocol
    host: 192.168.0.1
    port: 5010
    interval: 60
    mapping:
      read:
        - plc: M1000-M1099
          ld: M0
        - plc: D1000-D1099
          ld: D0
      write:
        - plc: M100-M199
          ld: M1100
        - plc: D100-D199
          ld: D1100
DOC

module LadderDrive
module Emulator

class PlcMapperPlugin < Plugin

  attr_reader :lock
  attr_reader :values_for_reading
  attr_reader :values_for_writing

  def initialize plc
    super #plc
    return if disabled?
    @lock = Mutex.new
    @values_for_reading = {}
    @values_for_writing = {}
    setup unless config.empty? || config[:disable]
  end

  def run_cycle plc
    return if disabled?
    #return false unless super
    @lock.synchronize {
      # set values from plcs to ladder drive.
      self.values_for_reading.each do |d, v|
        plc.device_by_name(d).value = v
      end
      self.values_for_reading.clear

      # set values from ladder drive to values_for_writing.
      # then set it to plc at #sync_with_plc
      self.values_for_writing.each do |d, v|
        dev = plc.device_by_name(d)
        self.values_for_writing[d] = dev.bit_device? ? dev.bool : dev.value
      end
    }
  end

  private

    def setup
      config[:plcs].each do |plc_config|
        Thread.start(plc_config) {|config|
          mapping_thread_proc config
        }
      end
    end

    def protocol_with_config config
      begin
        eval("#{config[:protocol].camelize}.new").tap do |protocol|
          protocol.host = config[:host] if config[:host]
          protocol.port = config[:port] if config[:port]
        end
      rescue
        nil
      end
    end

    def mapping_devices protocol, mappings
      mappings.map do |h|
        a = []
        h.each do |k, v|
          devs = v.split("-").map{|d|
            protocol.device_by_name d.strip
          }
          d1 = devs.first
          d2 = devs.last
          a << k
          a << [d1, [d2 - d1 + 1, 1].max]
        end
        Hash[*a]
      end
    end

    def mapping_thread_proc config
      protocol = protocol_with_config config

      read_mappings = mapping_devices protocol, config[:mapping][:read]
      write_mappings = mapping_devices protocol, config[:mapping][:write]

      interval = config[:interval]
      next_time = begin
        t = Time.now.to_f
        t = t - t % interval
        Time.at t
      end

      alerted = false
      loop do
        begin
          now = Time.now
          if next_time <= now
            sync_with_plc protocol, read_mappings, write_mappings
            next_time += interval
          end
          sleep [next_time - Time.now, 0].max
          alerted = false
        rescue => e
          puts "#{config[:description]} is not reachable. #{e}" unless alerted
puts $!
puts $@
          alerted = true
          protocol.close
        end
      end
    end

    def sync_with_plc protocol, read_mappings, write_mappings
      # set values from plc to values_for_reading.
      read_mappings.each do |mapping|
        src_d, c = mapping[:plc]
        values = protocol[src_d.name, c]
        dst_d = plc.device_by_name mapping[:ld].first.name
        lock.synchronize {
          values.each do |v|
            values_for_reading[dst_d.name] = v
            dst_d = dst_d.next_device
          end
        }
      end

      # set values from ladder drive (values_for_writing) to plc
      # values_for_writing was set at run_cycle
      # but for the first time, it's not known what device it should take.
      # after running below, devices for need is listed to values_for_writing.
      write_mappings.each do |mapping|
        dst_d, c = mapping[:plc]
        src_d = plc.device_by_name mapping[:ld].first.name
        values = []
        lock.synchronize {
          c.times do
            # It may not get the value for the first time, set zero instead of it.
            self.values_for_writing[src_d.name] ||= (src_d.bit_device? ? false : 0)
            values << self.values_for_writing[src_d.name]
            src_d = src_d.next_device
          end
        }
        protocol[dst_d.name, c] = values
      end
    end

end

end
end


def plugin_plc_mapper_init plc
  @plugin_plc_mapper = LadderDrive::Emulator::PlcMapperPlugin.new plc
end

def plugin_plc_mapper_exec plc
  begin
    @plugin_plc_mapper.run_cycle plc
  rescue
puts $!
puts $@
    plc.close
  end
end
