
require 'time'
require 'wab/impl'

module WAB
  module IO

    # A Shell that uses STDIN and STDOUT for all interactions with the View
    # and Model. Since the View and Model APIs are asynchronous and Controller
    # calls are synchronous for simplicity some effort is required to block
    # where needed to achieve the difference in behavior.
    class Shell
      include WAB::ShellLogger

      attr_reader :path_pos
      attr_reader :type_key
      attr_accessor :timeout

      # Sets up the shell with the designated number of processing threads and
      # the type_key.
      #
      # tcnt:: processing thread count
      # type_key:: key to use for the record type
      def initialize(tcnt, type_key='kind', path_pos=0)
        @controllers = {}
        @type_key = type_key
        @path_pos = path_pos
        @engine = Engine.new(self, tcnt)
        @timeout = 2.0
        @logger = Logger.new(STDERR)
        logger.level = Logger::WARN
      end

      # Starts listening and processing.
      def start()
        @engine.start
      end

      # Register a controller for a named type.
      #
      # If a request is received for an unregistered type the default controller
      # will be used. The default controller is registered with a +nil+ key.
      #
      # type:: type name
      # controller:: Controller instance for handling requests for the identified +type+
      def register_controller(type, controller)
        controller.shell = self
        @controllers[type] = controller
      end

      # Returns the controller associated with the type key found in the
      # data. If a controller has not be registered under that key the default
      # controller is returned if there is one.
      #
      # data:: data to extract the type from for lookup in the controllers
      def controller(data)
        path = data.get(:path)
        path = path.native if path.is_a?(WAB::Data)
        return path_controller(path) unless path.nil? || (path.length <= @path_pos)

        content = data.get(:content)
        return @controllers[content.get(@type_key)] || default_controller unless content.nil?

        default_controller
      end

      # Returns the controller according to the type in the path.
      #
      # path: path Array such as from a URL
      def path_controller(path)
        @controllers[path[@path_pos]] || default_controller
      end

      # Create and return a new data instance with the provided initial value.
      # The value must be a Hash or Array. The members of the Hash or Array
      # must be nil, boolean, String, Integer, Float, BigDecimal, Array, Hash,
      # Time, URI::HTTP, or WAB::UUID. Keys to Hashes must be Symbols.
      #
      # If the repair flag is true then an attempt will be made to fix the
      # value by replacing String keys with Symbols and calling to_h or to_s
      # on unsupported Objects.
      #
      # value:: initial value
      # repair:: flag indicating invalid value should be repaired if possible
      def data(value={}, repair=false)
        WAB::Impl::Data.new(value, repair)
      end

      ### View related methods.

      # Push changed data to the view if it matches one of the subscription
      # filters.
      #
      # data: Wab::Data to push to the view if subscribed
      def changed(_data)
        raise NotImplementedError.new
      end

      ### Model related methods.

      # Returns a WAB::Data that matches the object reference or nil if there
      # is no match.
      #
      # ref:: object reference
      def get(ref)
        result = query(where: ref.to_i, select: '$')
        raise WAB::Error.new("nil result get of #{ref}.") if result.nil?
        raise WAB::Error.new("error on get of #{ref}. #{result[:error]}") if 0 != result[:code]

        ra = result[:results]
        return nil if (ra.nil? || 0 == ra.length)
        ra[0]
      end

      # Evaluates the JSON TQL query. The TQL should be native Ruby objects
      # that correspond to the TQL JSON format but using Symbol keys instead
      # of strings.
      #
      # tql:: query to evaluate
      def query(tql)
        @engine.request(tql, @timeout)
      end

      # Subscribe to changes in stored data and push changes to the controller
      # if it passes the supplied filter.
      #
      # The +controller#changed+ method is called when changes in data cause
      # the associated object to pass the provided filter.
      #
      # controller:: the controller to notify of changed
      # filter:: the filter to apply to the data. Syntax is that TQL uses for the FILTER clause.
      def subscribe(_controller, _filter)
        raise NotImplementedError.new
      end

      private

      def default_controller
        @default_controller ||= @controllers[nil]
      end

    end # Shell
  end # IO
end # WAB
