require 'tubesock'
require 'tubesock/hijack'

module Entangled
  module Controller
    include Tubesock::Hijack
    
    module InstanceMethods
      include Entangled::Helpers

      private

      # The plural name of the resource, inferred from the
      # controller's name. For example, if it's the TacosController,
      # the resources_name will be "tacos". This is used to
      # infer the instance variable name for collections assigned
      # in the controller action
      def resources_name
        controller_name
      end

      # The singular name of the resource, inferred from the
      # resources_name. This is used to infer the instance
      # variable name for a single record assigned in the controller
      # action
      def resource_name
        resources_name.singularize
      end

      # The model for this controller. E.g. Taco for a TacosController
      def model
        controller_name.classify.constantize
      end

      # Grabs @tacos
      def collection
        instance_variable_get(:"@#{resources_name}")
      end

      # Grabs @taco
      def member
        instance_variable_get(:"@#{resource_name}")
      end

      # Infer channel from current path
      def channel
        request.path
      end

      # Close the connection to the DB so as to
      # not exceed the pool size. Otherwise, too many
      # connections will be leaked and the pool
      # will be exceeded
      def close_db_connection
        if ActiveRecord::Base.connection
          ActiveRecord::Base.connection.close
        end
      end

      # Broadcast events to every connected client
      def broadcast(&block)
        # Use hijack to handle sockets
        hijack do |tubesock|
          begin
            # Assuming restful controllers, the behavior of
            # this method has to change depending on the action
            # it's being used in
            case action_name

            # If the controller action is 'index', a collection
            # of records should be broadcast
            when 'index'
              execute_action(tubesock, block)

              # The following code will run if an instance
              # variable with the plural resource name has been
              # assigned in yield. For example, if a
              # TacosController's index action looked something
              # like this:

              # def index
              #   broadcast do
              #     @tacos = Taco.all
              #   end
              # end

              # ...then @tacos will be broadcast to all connected
              # clients. The variable name, in this example,
              # has to be "@tacos"
              if collection
                redis_thread = Thread.new do
                  redis.subscribe channel do |on|
                    # Broadcast messages to all connected clients
                    on.message do |channel, message|
                      tubesock.send_data message
                    end

                    # Send message to whoever just subscribed
                    tubesock.send_data({
                      resources: collection
                    }.to_json)

                    close_db_connection
                  end
                end

                # When client disconnects, kill the thread
                tubesock.onclose do
                  redis_thread.kill
                end
              end

            # If the controller's action name is 'show', a single record
            # should be broadcast
            when 'show'
              execute_action(tubesock, block)

              # The following code will run if an instance variable
              # with the singular resource name has been assigned in
              # yield. For example, if a TacosController's show action
              # looked something like this:

              # def show
              #   broadcast do
              #     @taco = Taco.find(params[:id])
              #   end
              # end

              # ...then @taco will be broadcast to all connected clients.
              # The variable name, in this example, has to be "@taco"
              if member
                redis_thread = Thread.new do
                  redis.subscribe channel do |on|
                    # Broadcast messages to all connected clients
                    on.message do |channel, message|
                      tubesock.send_data message
                    end

                    # Send message to whoever just subscribed
                    tubesock.send_data({
                      resource: member
                    }.to_json)

                    close_db_connection
                  end
                end

                # When client disconnects, kill the thread
                tubesock.onclose do
                  redis_thread.kill
                end
              end

            # If the controller's action name is 'create', a record should be
            # created. Before yielding, the params hash has to be prepared
            # with attributes sent to the socket. The actual publishing
            # happens in the model's callback
            when 'create'
              tubesock.onmessage do |m|
                set_resource_params(m)
                execute_action(tubesock, block)

                # Send resource that was just created back to client. The resource
                # on the client will be overridden with this one. This is important
                # so that the id, created_at and updated_at and possibly other
                # attributes arrive on the client
                if member
                  tubesock.send_data({
                    resource: member
                  }.to_json)
                end

                close_db_connection
              end

            # If the controller's action name is 'update', a record should be
            # updated. Before yielding, the params hash has to be prepared
            # with attributes sent to the socket
            when 'update'
              tubesock.onmessage do |m|
                set_resource_params(m)
                execute_action(tubesock, block)

                # Send resource that was just updated back to client. The resource
                # on the client will be overridden with this one. This is important
                # so that the new updated_at and possibly other attributes arrive
                # on the client
                if member
                  tubesock.send_data({
                    resource: member
                  }.to_json)
                end

                close_db_connection
              end

            when 'destroy'
              tubesock.onmessage do |m|
                execute_action(tubesock, block)

                # Send resource that was just destroyed back to client
                if member
                  tubesock.send_data({
                    resource: member
                  }.to_json)
                end

                close_db_connection
              end

            # For every other controller action, simply wrap whatever is
            # yielded in the tubesock block to execute it in the context
            # of the socket. Other custom actions can be added through this
            else
              tubesock.onmessage do |m|
                # If message was sent, attach to params (rescue exception if
                # message not valid JSON or message not present)
                params.merge!(JSON.parse(m)) rescue nil

                execute_action(tubesock, block)

                close_db_connection
              end
            end
          rescue Exception => e
            Rails.logger.error e
            close_db_connection
          end
        end
      end

      def set_resource_params(message_from_socket)
        params[resource_name.to_sym] = JSON.parse(message_from_socket)
      end

      # Run the actual controller action that is passed as
      # a block to the broadcast method and catch any exceptions
      # as needed
      def execute_action(tubesock, block)
        begin
          # Execute block
          block.call
        rescue Exception => e
          # Log the error
          Rails.logger.error e.message

          # Print stack strace
          puts e.backtrace

          # Send error message to client
          tubesock.send_data({
            error: e.message
          }.to_json)
        end
      end
    end
    
    def self.included(receiver)
      receiver.send :include, InstanceMethods
    end
  end
end
