#
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements.  See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership.  The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License.  You may obtain a copy of the License at
#
#   http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied.  See the License for the
# specific language governing permissions and limitations
# under the License.
#

require 'cqmf2'
require 'cqpid'
require 'thread'
require 'socket'
require 'monitor'

module Qmf2

  Cqmf2.constants.each do |c|
    if c.index('AGENT_') == 0 or c.index('CONSOLE_') == 0 or
        c.index('SCHEMA_') == 0 or c.index('SEV_') == 0 or c.index('QUERY') == 0
      const_set(c, Cqmf2.const_get(c))
    end
  end

  SCHEMA_TYPE_DATA  = Cqmf2::SCHEMA_TYPE_DATA()
  SCHEMA_TYPE_EVENT = Cqmf2::SCHEMA_TYPE_EVENT()

  SCHEMA_DATA_VOID   = Cqmf2::SCHEMA_DATA_VOID()
  SCHEMA_DATA_BOOL   = Cqmf2::SCHEMA_DATA_BOOL()
  SCHEMA_DATA_INT    = Cqmf2::SCHEMA_DATA_INT()
  SCHEMA_DATA_FLOAT  = Cqmf2::SCHEMA_DATA_FLOAT()
  SCHEMA_DATA_STRING = Cqmf2::SCHEMA_DATA_STRING()
  SCHEMA_DATA_MAP    = Cqmf2::SCHEMA_DATA_MAP()
  SCHEMA_DATA_LIST   = Cqmf2::SCHEMA_DATA_LIST()
  SCHEMA_DATA_UUID   = Cqmf2::SCHEMA_DATA_UUID()

  ACCESS_READ_CREATE = Cqmf2::ACCESS_READ_CREATE()
  ACCESS_READ_WRITE  = Cqmf2::ACCESS_READ_WRITE()
  ACCESS_READ_ONLY   = Cqmf2::ACCESS_READ_ONLY()

  DIR_IN     = Cqmf2::DIR_IN()
  DIR_OUT    = Cqmf2::DIR_OUT()
  DIR_IN_OUT = Cqmf2::DIR_IN_OUT()

  SEV_EMERG  = Cqmf2::SEV_EMERG()
  SEV_ALERT  = Cqmf2::SEV_ALERT()
  SEV_CRIT   = Cqmf2::SEV_CRIT()
  SEV_ERROR  = Cqmf2::SEV_ERROR()
  SEV_WARN   = Cqmf2::SEV_WARN()
  SEV_NOTICE = Cqmf2::SEV_NOTICE()
  SEV_INFORM = Cqmf2::SEV_INFORM()
  SEV_DEBUG  = Cqmf2::SEV_DEBUG()

  ##==============================================================================
  ## EXCEPTIONS
  ##==============================================================================

  class QmfAgentException < RuntimeError
    attr :data

    def initialize(data)
      @data = data
    end

    def to_s
      "QmfAgentException: #{@data}"
    end
  end

  ##==============================================================================
  ## AGENT HANDLER
  ##==============================================================================

  class AgentHandler

    def initialize(session)
      @_session = session
      @_running = false
      @_thread  = nil
    end

    ##
    ## Call the "start" method to run the handler on a new thread.
    ##
    def start
      @_thread = Thread.new do
        run
      end
    end

    ##
    ## Request that the running thread complete and exit.
    ##
    def cancel
      @_running = false
      @_thread.join if @_thread
      @_thread = nil
    end

    ##
    ## Call the "run" method only if you want the handler to run on your own thread.
    ##
    def run
      @_running = true
      event = Cqmf2::AgentEvent.new
      while @_running do
        valid = @_session.impl.nextEvent(event, Cqpid::Duration.SECOND)
        if valid and @_running
          case event.getType
          when Cqmf2::AGENT_AUTH_QUERY
            yes = authorize_query(Query.new(event.getQuery()), event.getUserId())
            if yes == true
              @_session.impl.authAccept(event)
            else
              @_session.impl.authReject(event)
            end

          when Cqmf2::AGENT_QUERY
            context = QueryContext.new(@_session, event)
            get_query(context, Query.new(event.getQuery()), event.getUserId())

          when Cqmf2::AGENT_METHOD
            context = MethodContext.new(@_session, event)
            begin
              method_call(context, event.getMethodName(), event.getDataAddr(), event.getArguments(), event.getUserId())
            rescue Exception => ex
              @_session.impl.raiseException(event, "#{ex}")
            end

          end
        end
      end
    end


    ##
    ## The following methods are intended to be overridden in a sub-class.  They are
    ## handlers for events that occur on QMF consoles.
    ##

    #
    # This method will only be invoked if the "allow-queries" option is enabled on the
    # agent session.  When invoked, it provides the query and the authenticated user-id
    # of the querying client.
    #
    # This method must return true if the query is permitted, false otherwise.
    #
    def authorize_query(query, user_id); end

    #
    # This method will only be invoked if the "external" option is "True" on the agent
    # session.  When invoked, the method should begin the process of responding to a data
    # query.  The authenticated user-id of the requestor is provided for informational
    # purposes.  The 'context' variable is used to provide the results back to the requestor.
    #
    # For each matching Data object, call context.response(data).  When the query is complete,
    # call context.complete().  After completing the query, you should not use 'context' any
    # longer.
    #
    # Note: It is not necessary to process the query synchronously.  If desired, this method
    # may store the context for asynchronous processing or pass it to another thread for
    # processing.  There is no restriction on the number of contexts that may be in-flight
    # concurrently.
    #
    def get_query(context, query, user_id); end

    #
    # This method is invoked when a console calls a QMF method on the agent.  Supplied are
    # a context for the response, the method name, the data address of the data object being
    # called, the input arguments (a dictionary), and the caller's authenticated user-id.
    #
    # A method call can end one of two ways:  Successful completion, in which the output
    # arguments (if any) are supplied; and Exceptional completion if there is an error.
    #
    # Successful Completion:
    #    For each output argument, assign the value directly to context (context.arg1 = "value")
    #    Once arguments are assigned, call context._success().
    #
    # Exceptional Completion:
    #    Method 1:  Call context._exception(data) where 'data' is a string or a Data object.
    #    Method 2:  Raise an exception (raise "Error Text") synchronously in the method body.
    #
    # Note: Like get_query, method_call may process methods synchronously or asynchronously.
    # This method may store the context for later asynchronous processing.  There is no
    # restriction on the number of contexts that may be in-flight concurrently.
    #
    # However, "Method 2" for Exceptional Completion can only be done synchronously.
    #
    def method_call(context, method_name, data_addr, args, user_id); end
  end

  class QueryContext
    def initialize(agent, context)
      @agent = agent
      @context = context
    end

    def response(data)
      @agent.impl.response(@context, data.impl)
    end

    def complete
      @agent.impl.complete(@context)
    end
  end

  class MethodContext
    def initialize(agent, context)
      @agent = agent
      @context = context
    end

    def _success
      @agent.impl.methodSuccess(@context)
    end

    def _exception(ex)
      if ex.class == Data
        @agent.impl.raiseException(@context, ex.impl)
      else
        @agent.impl.raiseException(@context, ex)
      end
    end

    def method_missing(name_in, *args)
      name = name_in.to_s
      if name[name.length - 1] == 61
        name = name[0..name.length - 2]
        @context.impl.addReturnArgument(name, args[0])
      else
        super.method_missing(name_in, args)
      end
    end
  end

  ##==============================================================================
  ## CONSOLE HANDLER
  ##==============================================================================

  class ConsoleHandler

    def initialize(session)
      @_session = session
      @_running = false
      @_thread  = nil
    end

    ##
    ## Call the "start" method to run the handler on a new thread.
    ##
    def start
      @_thread = Thread.new do
        run
      end
    end

    ##
    ## Request that the running thread complete and exit.
    ##
    def cancel
      @_running = false
      @_thread.join if @_thread
      @_thread = nil
    end

    ##
    ## Call the "run" method only if you want the handler to run on your own thread.
    ##
    def run
      @_running = true
      event = Cqmf2::ConsoleEvent.new
      while @_running do
        valid = @_session.impl.nextEvent(event, Cqpid::Duration.SECOND)
        if valid and @_running
          case event.getType
          when Cqmf2::CONSOLE_AGENT_ADD
            agent_added(Agent.new(event.getAgent))

          when Cqmf2::CONSOLE_AGENT_DEL
            reason = :filter
            reason = :aged if event.getAgentDelReason == Cqmf2::AGENT_DEL_AGED
            agent_deleted(Agent.new(event.getAgent), reason)

          when Cqmf2::CONSOLE_AGENT_RESTART
            agent_restarted(Agent.new(event.getAgent))

          when Cqmf2::CONSOLE_AGENT_SCHEMA_UPDATE
            agent_schema_updated(Agent.new(event.getAgent))

          when Cqmf2::CONSOLE_EVENT
            event_raised(Agent.new(event.getAgent), Data.new(event.getData(0)), event.getTimestamp, event.getSeverity)

          end
        end
      end
    end


    ##
    ## The following methods are intended to be overridden in a sub-class.  They are
    ## handlers for events that occur on QMF consoles.
    ##

    #
    # A new agent, whose attributes match the console's agent filter, has been discovered.
    #
    def agent_added(agent); end

    #
    # A known agent has been removed from the agent list.  There are two possible reasons
    # for agent deletion:
    #
    #    1) :aged   - The agent hasn't been heard from for the maximum age interval and is
    #                 presumed dead.
    #    2) :filter - The agent no longer matches the console's agent-filter and has been
    #                 effectively removed from the agent list.  Such occurrences are likely
    #                 to be seen immediately after setting the filter to a new value.
    #
    def agent_deleted(agent, reason); end

    #
    # An agent-restart was detected.  This occurs when the epoch number advertised by the
    # agent changes.  It indicates that the agent in question was shut-down/crashed and
    # restarted.
    #
    def agent_restarted(agent); end

    #
    # The agent has registered new schema information which can now be queried, if desired.
    #
    def agent_schema_updated(agent); end

    #
    # An agent raised an event.  The 'data' argument is a Data object that contains the
    # content of the event.
    #
    def event_raised(agent, data, timestamp, severity); end
  end

  ##==============================================================================
  ## CONSOLE SESSION
  ##==============================================================================

  class ConsoleSession
    attr_reader :impl

    ## The options string is of the form "{key:value,key:value}".  The following keys are supported:
    ##
    ##    domain:NAME                   - QMF Domain to join [default: "default"]
    ##    max-agent-age:N               - Maximum time, in minutes, that we will tolerate not hearing from
    ##                                    an agent before deleting it [default: 5]
    ##    listen-on-direct:{True,False} - If True:  Listen on legacy direct-exchange address for backward compatibility [default]
    ##                                    If False: Listen only on the routable direct address
    ##    strict-security:{True,False}  - If True:  Cooperate with the broker to enforce strict access control to the network
    ##                                  - If False: Operate more flexibly with regard to use of messaging facilities [default]
    ##
    def initialize(connection, options="")
      @impl = Cqmf2::ConsoleSession.new(connection, options)
    end

    def set_domain(domain)        @impl.setDomain(domain)      end
    def set_agent_filter(filter)  @impl.setAgentFilter(filter) end

    def open()   @impl.open  end
    def close()  @impl.close  end

    def agents
      result = []
      count = @impl.getAgentCount
      for i in 0...count
        result << Agent.new(@impl.getAgent(i))
      end
      return result
    end

    def connected_broker_agent
      Agent.new(@impl.getConnectedBrokerAgent)
    end
  end

  ##==============================================================================
  ## AGENT SESSION
  ##==============================================================================

  class AgentSession
    attr_reader :impl

    ## The options string is of the form "{key:value,key:value}".  The following keys are supported:
    ##
    ##    interval:N                 - Heartbeat interval in seconds [default: 60]
    ##    external:{True,False}      - Use external data storage (queries and subscriptions are pass-through) [default: False]
    ##    allow-queries:{True,False} - If True:  automatically allow all queries [default]
    ##                                 If False: generate an AUTH_QUERY event to allow per-query authorization
    ##    allow-methods:{True,False} - If True:  automatically allow all methods [default]
    ##                                 If False: generate an AUTH_METHOD event to allow per-method authorization
    ##    max-subscriptions:N        - Maximum number of concurrent subscription queries permitted [default: 64]
    ##    min-sub-interval:N         - Minimum publish interval (in milliseconds) permitted for a subscription [default: 3000]
    ##    sub-lifetime:N             - Lifetime (in seconds with no keepalive) for a subscription [default: 300]
    ##    public-events:{True,False} - If True:  QMF events are sent to the topic exchange [default]
    ##                                 If False: QMF events are only sent to authorized subscribers
    ##    listen-on-direct:{True,False} - If True:  Listen on legacy direct-exchange address for backward compatibility [default]
    ##                                    If False: Listen only on the routable direct address
    ##    strict-security:{True,False}  - If True:  Cooperate with the broker to enforce strict access control to the network
    ##                                  - If False: Operate more flexibly with regard to use of messaging facilities [default]
    ##
    def initialize(connection, options="")
      @impl = Cqmf2::AgentSession.new(connection, options)
    end

    def set_domain(val)         @impl.setDomain(val)           end
    def set_vendor(val)         @impl.setVendor(val)           end
    def set_product(val)        @impl.setProduct(val)          end
    def set_instance(val)       @impl.setInstance(val)         end
    def set_attribute(key, val) @impl.setAttribute(key, val)   end
    def open()                  @impl.open                     end
    def close()                 @impl.close                    end
    def register_schema(cls)    @impl.registerSchema(cls.impl) end

    def add_data(data, name="", persistent=false)
      DataAddr.new(@impl.addData(data.impl, name, persistent))
    end

    def del_data(addr)
      @impl.del_data(addr.impl)
    end

    def raise_event(data, severity=nil)
      if !severity
        @impl.raiseEvent(data.impl)
      else
        @impl.raiseEvent(data.impl, severity)
      end
    end
  end

  ##==============================================================================
  ## AGENT PROXY
  ##==============================================================================

  class Agent
    attr_reader :impl

    def initialize(impl)
      @impl = impl
    end

    def name()       @impl.getName        end
    def epoch()      @impl.getEpoch       end
    def vendor()     @impl.getVendor      end
    def product()    @impl.getProduct     end
    def instance()   @impl.getInstance    end
    def attributes() @impl.getAttributes  end

    def to_s
      "#{vendor}:#{product}:#{instance}"
    end

    def query(q, timeout=30)
      if q.class == Query
        q_arg = q.impl
      else
        q_arg = q
      end
      dur = Cqpid::Duration.new(Cqpid::Duration.SECOND.getMilliseconds * timeout)
      result = @impl.query(q_arg, dur)
      raise QmfAgentException.new(Data.new(result.getData(0))) if result.getType == Cqmf2::CONSOLE_EXCEPTION
      raise "Protocol error, expected CONSOLE_QUERY_RESPONSE, got #{result.getType}" if result.getType != Cqmf2::CONSOLE_QUERY_RESPONSE
      data_list = []
      count = result.getDataCount
      for i in 0...count
        data_list << Data.new(result.getData(i))
      end
      return data_list
    end

    def load_schema_info(timeout=30)
      dur = Cqpid::Duration.new(Cqpid::Duration.SECOND.getMilliseconds * timeout)
      @impl.querySchema(dur)
    end

    def packages
      result = []
      count = @impl.getPackageCount
      for i in 0...count
        result << @impl.getPackage(i)
      end
      return result
    end

    def schema_ids(package)
      result = []
      count = @impl.getSchemaIdCount(package)
      for i in 0...count
        result << SchemaId.new(@impl.getSchemaId(package, i))
      end
      return result
    end

    def schema(sid, timeout=30)
      dur = Cqpid::Duration.new(Cqpid::Duration.SECOND.getMilliseconds * timeout)
      Schema.new(@impl.getSchema(sid.impl, dur))
    end
  end

  ##==============================================================================
  ## QUERY
  ##==============================================================================

  class Query
    attr_reader :impl
    def initialize(arg1, arg2=nil, arg3=nil)
      if arg1.class == Qmf2::DataAddr
        @impl = Cqmf2::Query.new(arg1.impl)
      end
    end

    def addr()      DataAddr.new(@impl.getDataAddr())  end
    def schema_id() SchemaId.new(@impl.getSchemaId())  end
    def predicate() @impl.getPredicate()               end

    def matches?(data)
      map = data
      map = data.properties if data.class == Data
      @impl.matchesPredicate(map)
    end
  end

  ##==============================================================================
  ## DATA
  ##==============================================================================

  class Data
    attr_reader :impl

    def initialize(arg=nil)
      @schema = nil
      if arg == nil
        @impl = Cqmf2::Data.new
      elsif arg.class == Cqmf2::Data
        @impl = arg
      elsif arg.class == Schema
        @impl = Cqmf2::Data.new(arg.impl)
        @schema = arg
      else
        raise "Unsupported initializer for Data"
      end
    end

    def to_s
      "#{@impl.getProperties}"
    end

    def schema_id
      if @impl.hasSchema
        return SchemaId.new(@impl.getSchemaId)
      end
      return nil
    end

    def set_addr(addr)
      @impl.setAddr(addr.impl)
    end

    def addr
      if @impl.hasAddr
        return DataAddr.new(@impl.getAddr)
      end
      return nil
    end

    def agent
      return Agent.new(@impl.getAgent)
    end

    def update(timeout=5)
      dur = Cqpid::Duration.new(Cqpid::Duration.SECOND.getMilliseconds * timeout)
      agent = @impl.getAgent
      query = Cqmf2::Query.new(@impl.getAddr)
      result = agent.query(query, dur)
      raise "Update query failed" if result.getType != Cqmf2::CONSOLE_QUERY_RESPONSE
      raise "Object no longer exists on agent" if result.getDataCount == 0
      @impl = Cqmf2::Data.new(result.getData(0))
      return nil
    end

    def properties
      return @impl.getProperties
    end

    def get_attr(name)
      @impl.getProperty(name)
    end

    def set_attr(name, v)
      @impl.setProperty(name, v)
    end

    def [](name)
      get_attr(name)
    end

    def []=(name, value)
      set_attr(name, value)
    end

    def _get_schema
      unless @schema
        raise "Data object has no schema" unless @impl.hasSchema
        @schema = Schema.new(@impl.getAgent.getSchema(@impl.getSchemaId))
      end
    end

    def method_missing(name_in, *args)
      #
      # Convert the name to a string and determine if it represents an
      # attribute assignment (i.e. "attr=")
      #
      name = name_in.to_s
      attr_set = (name[name.length - 1] == 61)
      name = name[0..name.length - 2] if attr_set

      #
      # We'll be needing the schema to determine how to proceed.  Get the schema.
      # Note that this call may block if the remote agent needs to be queried
      # for the schema (i.e. the schema isn't in the local cache).
      #
      _get_schema

      #
      # If the name matches a property name, set or return the value of the property.
      #
      @schema.properties.each do |prop|
        if prop.name == name
          if attr_set
            return set_attr(name, args[0])
          else
            return get_attr(name)
          end
        end
      end

      #
      # If we still haven't found a match for the name, check to see if
      # it matches a method name.  If so, marshall the arguments and invoke
      # the method.
      #
      @schema.methods.each do |method|
        if method.name == name
          raise "Sets not permitted on methods" if attr_set
          result = @impl.getAgent.callMethod(name, _marshall(method, args), @impl.getAddr)
          if result.getType == Cqmf2::CONSOLE_EXCEPTION
            raise QmfAgentException, result.getData(0)
          end
          return result.getArguments
        end
      end

      #
      # This name means nothing to us, pass it up the line to the parent
      # class's handler.
      #
      super.method_missing(name_in, args)
    end

    #
    # Convert a Ruby array of arguments (positional) into a Value object of type "map".
    #
    private
    def _marshall(schema, args)
      count = 0
      schema.arguments.each do |arg|
        if arg.direction == DIR_IN || arg.direction == DIR_IN_OUT
          count += 1
        end
      end
      raise "Wrong number of arguments: expecter #{count}, got #{arge.length}" if count != args.length
      map = {}
      count = 0
      schema.arguments.each do |arg|
        if arg.direction == DIR_IN || arg.direction == DIR_IN_OUT
          map[arg.name] = args[count]
          count += 1
        end
      end
      return map
    end
  end

  ##==============================================================================
  ## DATA ADDRESS
  ##==============================================================================

  class DataAddr
    attr_reader :impl

    def initialize(arg, agentName="")
      if arg.class == Hash
        @impl = Cqmf2::DataAddr.new(arg)
      elsif arg.class == Cqmf2::DataAddr
        @impl = arg
      else
        @impl = Cqmf2::DataAddr.new(arg, agentName)
      end
    end

    def ==(other)
      return @impl == other.impl
    end

    def as_map()       @impl.asMap          end
    def agent_name()   @impl.getAgentName   end
    def name()         @impl.getName        end
    def agent_epoch()  @impl.getAgentEpoch  end
  end

  ##==============================================================================
  ## SCHEMA ID
  ##==============================================================================

  class SchemaId
    attr_reader :impl
    def initialize(impl)
      @impl = impl
    end

    def type()         @impl.getType         end
    def package_name() @impl.getPackageName  end
    def class_name()   @impl.getName         end
    def hash()         @impl.getHash         end
  end

  ##==============================================================================
  ## SCHEMA
  ##==============================================================================

  class Schema
    attr_reader :impl
    def initialize(arg, packageName="", className="", kwargs={})
      if arg.class == Cqmf2::Schema
        @impl = arg
      else
        @impl = Cqmf2::Schema.new(arg, packageName, className)
        @impl.setDesc(kwargs[:desc]) if kwargs.include?(:desc)
        @impl.setDefaultSeverity(kwargs[:sev]) if kwargs.include?(:sev)
      end
    end

    def finalize()         @impl.finalize                   end
    def schema_id()        SchemaId.new(@impl.getSchemaId)  end
    def desc()             @impl.getDesc                    end
    def sev()              @impl.getDefaultSeverity         end
    def add_property(prop) @impl.addProperty(prop.impl)     end
    def add_method(meth)   @impl.addMethod(meth.impl)       end

    def properties
      result = []
      count = @impl.getPropertyCount
      for i in 0...count
        result << SchemaProperty.new(@impl.getProperty(i))
      end
      return result
    end

    def methods
      result = []
      count = @impl.getMethodCount
      for i in 0...count
        result << SchemaMethod.new(@impl.getMethod(i))
      end
      return result
    end
  end

  ##==============================================================================
  ## SCHEMA PROPERTY
  ##==============================================================================

  class SchemaProperty
    attr_reader :impl

    def initialize(arg, dtype=nil, kwargs={})
      if arg.class == Cqmf2::SchemaProperty
        @impl = arg
      else
        @impl = Cqmf2::SchemaProperty.new(arg, dtype)
        @impl.setAccess(kwargs[:access])       if kwargs.include?(:access)
        @impl.setIndex(kwargs[:index])         if kwargs.include?(:index)
        @impl.setOptional(kwargs[:optional])   if kwargs.include?(:optional)
        @impl.setUnit(kwargs[:unit])           if kwargs.include?(:unit)
        @impl.setDesc(kwargs[:desc])           if kwargs.include?(:desc)
        @impl.setSubtype(kwargs[:subtype])     if kwargs.include?(:subtype)
        @impl.setDirection(kwargs[:direction]) if kwargs.include?(:direction)
      end
    end

    def name()       @impl.getName       end
    def access()     @impl.getAccess     end 
    def index?()     @impl.isIndex       end
    def optional?()  @impl.isOptional    end
    def unit()       @impl.getUnit       end
    def desc()       @impl.getDesc       end
    def subtype()    @impl.getSubtype    end
    def direction()  @impl.getDirection  end

    def to_s
      name
    end
  end

  ##==============================================================================
  ## SCHEMA METHOD
  ##==============================================================================

  class SchemaMethod
    attr_reader :impl

    def initialize(arg, kwargs={})
      if arg.class == Cqmf2::SchemaMethod
        @impl = arg
      else
        @impl = Cqmf2::SchemaMethod.new(arg)
        @impl.setDesc(kwargs[:desc]) if kwargs.include?(:desc)
      end
    end

    def name()  @impl.getName  end
    def desc()  @impl.getDesc  end
    def add_argument(arg)  @impl.addArgument(arg.impl)  end

    def arguments
      result = []
      count = @impl.getArgumentCount
      for i in 0...count
        result << SchemaProperty.new(@impl.getArgument(i))
      end
      return result
    end

    def to_s
      name
    end
  end
end


