#!/usr/bin/env ruby
#
# This is a 'fake' MQTT server to help with testing client implementations
#
# See https://github.com/njh/ruby-mqtt/blob/master/spec/fake_server.rb
#
# It behaves in the following ways:
#   * Responses to CONNECT with a successful CONACK
#   * Responses to PUBLISH by echoing the packet back
#   * Responses to SUBSCRIBE with SUBACK and a PUBLISH to the topic
#   * Responses to PINGREQ with PINGRESP
#   * Responses to DISCONNECT by closing the socket
#
# It has the following restrictions
#   * Doesn't deal with timeouts
#   * Only handles a single connection at a time
#

$:.unshift File.dirname(__FILE__)+'/../lib'

require 'logger'
require 'socket'
require 'mqtt'


class MQTT::FakeServer
  attr_reader :address, :port
  attr_reader :last_publish
  attr_reader :thread
  attr_reader :pings_received
  attr_accessor :just_one
  attr_accessor :logger

  # Create a new fake MQTT server
  #
  # If no port is given, bind to a random port number
  # If no bind address is given, bind to localhost
  def initialize(bind_address='127.0.0.1')
    @address = bind_address
  end

  # Get the logger used by the server
  def logger
    @logger ||= Logger.new(STDOUT)
  end

  # Start the thread and open the socket that will process client connections
  def start
    @socket ||= TCPServer.new(@address, 0)
    @address = @socket.addr[3]
    @port = @socket.addr[1]
    @thread ||= Thread.new do
      logger.info "Started a fake MQTT server on #{@address}:#{@port}"
      @times = 0
      loop do
        @times += 1
        # Wait for a client to connect
        client = @socket.accept
        @pings_received = 0
        handle_client(client)
        break if just_one
      end
    end
  end

  # Stop the thread and close the socket
  def stop
    logger.info "Stopping fake MQTT server"
    @socket.close unless @socket.nil?
    @socket = nil

    @thread.kill if @thread and @thread.alive?
    @thread = nil
  end

  # Start the server thread and wait for it to finish (possibly never)
  def run
    start
    begin
      @thread.join
    rescue Interrupt
      stop
    end
  end


  protected

  # Given a client socket, process MQTT packets from the client
  def handle_client(client)
    loop do
      packet = MQTT::Packet.read(client)
      logger.debug packet.inspect

      case packet
        when MQTT::Packet::Connect
          client.write MQTT::Packet::Connack.new(:return_code => 0)
        when MQTT::Packet::Publish
          client.write packet
          @last_publish = packet
        when MQTT::Packet::Subscribe
          client.write MQTT::Packet::Suback.new(
            :message_id => packet.message_id,
            :granted_qos => 0
          )
          topic = packet.topics[0][0]
          case @times
          when 1, ->x { x >= 3 }
            # Deliver retained messages
            client.write MQTT::Packet::Publish.new(
              :topic => topic,
              :payload => "did you know about #{topic}",
              :retain => true
            )
            client.write MQTT::Packet::Publish.new(
              :topic => topic,
              :payload => "hello #{topic}",
              :retain => true
            )
          when 2
            # Deliver a still retained message
            client.write MQTT::Packet::Publish.new(
              :topic => topic,
              :payload => "hello #{topic}",
              :retain => true
            )
            # Deliver a fresh message
            client.write MQTT::Packet::Publish.new(
              :topic => topic,
              :payload => "did you know about #{topic}",
              :retain => false
            )
          end

        when MQTT::Packet::Pingreq
          client.write MQTT::Packet::Pingresp.new
          @pings_received += 1
        when MQTT::Packet::Disconnect
          client.close
        break
      end
    end

    rescue MQTT::ProtocolException => e
      logger.warn "Protocol error, closing connection: #{e}"
      client.close
  end

end

if __FILE__ == $0
  server = MQTT::FakeServer.new(MQTT::DEFAULT_PORT)
  server.logger.level = Logger::DEBUG
  server.run
end
