# coding: utf-8
# This file is part of PacketGen
# See https://github.com/sdaubert/packetgen for more informations
# Copyright (C) 2016 Sylvain Daubert <sylvain.daubert@laposte.net>
# This program is published under MIT license.

module PacketGen
  module Header

    # Dissect error
    class DissectError < ParseError; end

    # Simple Network Management Protocol (SNMP)
    # @author Sylvain Daubert
    # @since 2.0.0
    class SNMP < ASN1Base

      # Agents listen to this port
      UDP_PORT1 = 161
      # Configuration sinks listen to this port
      UDP_PORT2 = 162

      PDU_GET      = 0
      PDU_NEXT     = 1
      PDU_RESPONSE = 2
      PDU_SET      = 3
      PDU_TRAPv1   = 4
      PDU_BULK     = 5
      PDU_INFORM   = 6
      PDU_TRAPv2   = 7
      PDU_REPORT   = 8

      ERRORS = { 'no_error'              => 0,
                 'too_big'               => 1,
                 'no_such_name'          => 2,
                 'bad_value'             => 3,
                 'read_only'             => 4,
                 'generic_error'         => 5,
                 'no_access'             => 6,
                 'wrong_type'            => 7,
                 'wrong_length'          => 8,
                 'wrong_encoding'        => 9,
                 'wrong_value'           => 10,
                 'no_creation'           => 11,
                 'inconsistent_value'    => 12,
                 'ressource_unavailable' => 13,
                 'commit_failed'         => 14,
                 'undo_failed'           => 15,
                 'authorization_error'   => 16,
                 'not_writable'          => 17,
                 'inconsistent_name'     => 18
               }

      # Class to handle SNMP VarBind
      #  VarBind ::= SEQUENCE {
      #                name  OBJECT IDENTIFIER,
      #                value ANY     -- depends on name
      #              }
      # @author Sylvain Daubert
      class VarBind < RASN1::Model
        sequence :varbind,
                 content: [objectid(:name),
                           any(:value)]
      end

      # Class to handle SNMP VariableBindingsList
      #  VarBindList ::= SEQUENCE (SIZE (0..max-bindings)) OF VarBind
      # @author Sylvain Daubert
      class VariableBindings < RASN1::Model
        sequence_of :bindings, VarBind
      end

      # Class to handle GetRequest PDU
      #  GetRequest-PDU ::= [0] IMPLICIT PDU
      #  
      #  PDU ::= SEQUENCE {
      #              request-id INTEGER (-214783648..214783647),
      #  
      #              error-status                -- sometimes ignored
      #                  INTEGER {
      #                      noError(0),
      #                      tooBig(1),
      #                      noSuchName(2),      -- for proxy compatibility
      #                      badValue(3),        -- for proxy compatibility
      #                      readOnly(4),        -- for proxy compatibility
      #                      genErr(5),
      #                      noAccess(6),
      #                      wrongType(7),
      #                      wrongLength(8),
      #                      wrongEncoding(9),
      #                      wrongValue(10),
      #                      noCreation(11),
      #                      inconsistentValue(12),
      #                      resourceUnavailable(13),
      #                      commitFailed(14),
      #                      undoFailed(15),
      #                      authorizationError(16),
      #                      notWritable(17),
      #                      inconsistentName(18)
      #                  },
      #          
      #              error-index                 -- sometimes ignored
      #                  INTEGER (0..max-bindings),
      #          
      #              variable-bindings           -- values are sometimes ignored
      #                  VarBindList
      #          }
      # @author Sylvain Daubert
      class GetRequest < RASN1::Model
        sequence :pdu,
                 implicit: SNMP::PDU_GET, constructed: true,
                 content: [integer(:id, value: 0),
                           enumerated(:error, enum: ERRORS),
                           integer(:error_index),
                           model(:varbindlist, VariableBindings)]

        # @return [String]
        def inspect
          Inspect.inspect_body(to_der, self.class)
        end
      end

      # Class to handle GetNextRequest PDU
      #  GetNextRequest-PDU ::= [1] IMPLICIT PDU   -- PDU definition: see GetRequest
      # @author Sylvain Daubert
      class GetNextRequest < GetRequest
        root_options implicit: SNMP::PDU_NEXT
      end

      # Class to handle GetResponse PDU
      #  GetResponse-PDU ::= [2] IMPLICIT PDU   -- PDU definition: see GetRequest
      # @author Sylvain Daubert
      class GetResponse < GetRequest
        root_options implicit: SNMP::PDU_RESPONSE
      end

      # Class to handle SetRequest PDU
      #  SetRequest-PDU ::= [3] IMPLICIT PDU   -- PDU definition: see GetRequest
      # @author Sylvain Daubert
      class SetRequest < GetRequest
        root_options implicit: SNMP::PDU_GET
      end

      # Class to handle Trap from SNMPv1
      #  Trap-PDU ::= [4] IMPLICIT SEQUENCE {
      #                          enterprise OBJECT IDENTIFIER,
      #                          agent-addr NetworkAddress,
      #                          generic-trap      -- generic trap type
      #                              INTEGER {
      #                                  coldStart(0),
      #                                  warmStart(1),
      #                                  linkDown(2),
      #                                  linkUp(3),
      #                                  authenticationFailure(4),
      #                                  egpNeighborLoss(5),
      #                                  enterpriseSpecific(6)
      #                              },
      #                          specific-trap INTEGER,
      #                          time-stamp TimeTicks,
      #                          variable-bindings VarBindList
      #                   }
      class Trapv1 < RASN1::Model
        sequence :trap,
                 implicit: SNMP::PDU_TRAPv1, constructed: true,
                 content: [objectid(:enterprise),
                           octet_string(:agent_addr),
                           enumerated(:generic_trap, enum: { 'cold_start'        => 0,
                                                             'warm_start'        => 1,
                                                             'link_down'         => 2,
                                                             'link_up'           => 3,
                                                             'auth_failure'      => 4,
                                                             'egp_neighbor_loss' => 5,
                                                             'specific'          => 6 }),
                           integer(:specific_trap),
                           integer(:timestamp),
                           model(:varbindlist, VariableBindings)]
      end

      # Class to handle Bulk PDU
      #  GetBulkRequest-PDU ::= [5] IMPLICIT BulkPDU
      #  
      #  BulkPDU ::=                         -- must be identical in
      #        SEQUENCE {                    -- structure to PDU
      #            request-id      INTEGER (-214783648..214783647),
      #            non-repeaters   INTEGER (0..max-bindings),
      #            max-repetitions INTEGER (0..max-bindings),
      #            variable-bindings           -- values are ignored
      #                VarBindList
      #        }
      # @author Sylvain Daubert
      class Bulk < RASN1::Model
        sequence :bulkpdu,
                 implicit: SNMP::PDU_BULK, constructed: true,
                 content: [integer(:id, value: 0),
                           integer(:non_repeaters),
                           integer(:max_repetitions),
                           model(:varbindlist, VariableBindings)]

        # @return [String]
        def inspect
          Inspect.inspect_body(to_der, self.class)
        end
      end

      # Class to handle InformRequest PDU
      #  InformRequest-PDU ::= [6] IMPLICIT PDU   -- PDU definition: see GetRequest
      # @author Sylvain Daubert
      class InformRequest < GetRequest
        root_options implicit: SNMP::PDU_INFORM
      end

      # Class to handle Trapv2 PDU
      #  SNMPv2-Trap-PDU ::= [7] IMPLICIT PDU   -- PDU definition: see GetRequest
      # @author Sylvain Daubert
      class Trapv2 < GetRequest
        root_options implicit: SNMP::PDU_TRAPv2
      end

      # Class to handle Report PDU
      #  Report-PDU ::= [8] IMPLICIT PDU   -- PDU definition: see GetRequest
      # @author Sylvain Daubert
      class Report < GetRequest
        root_options implicit: SNMP::PDU_REPORT
      end

      # Class to handle PDUs from SNMP packet
      #  PDUs ::= CHOICE {
      #             get-request      [0] IMPLICIT PDU,
      #             get-next-request [1] IMPLICIT PDU,
      #             get-response     [2] IMPLICIT PDU,
      #             set-request      [3] IMPLICIT PDU,
      #             snmpV1-trap      [4] IMPLICIT PDU,
      #             get-bulk-request [5] IMPLICIT PDU,
      #             inform-request   [6] IMPLICIT PDU,
      #             snmpV2-trap      [7] IMPLICIT PDU,
      #             report           [8] IMPLICIT PDU
      #           }
      # @author Sylvain Daubert
      class PDUs < RASN1::Model
        choice :pdus,
               content: [model(:get_request, GetRequest),
                         model(:get_next_request, GetNextRequest),
                         model(:get_response, GetResponse),
                         model(:set_request, SetRequest),
                         #model(:trapv1, Trapv1),
                         model(:bulk, Bulk),
                         model(:inform, InformRequest),
                         model(:trapv2, Trapv2),
                         model(:report, Report)]
      end

      sequence :message,
               content: [enumerated(:version, value: 'v2c',
                                    enum: { 'v1' => 0, 'v2c' => 1, 'v2' => 2, 'v3' => 3 }),
                         octet_string(:community, value: 'public'),
                         model(:data, PDUs)]

      define_attributes :version, :community

      # accessor to data payload
      # @return [GetRequest]
      def data
        @elements[:data]
      end

      def inspect
        str = super
        str << Inspect.shift_level(2)
        if self[:data].chosen.nil?
          str << Inspect::FMT_ATTR % [self[:data].type, :data, '']
        else
          data = self[:data]
          str << Inspect::FMT_ATTR % [data.type, :data, data.chosen_value.type]
          str << data.chosen_value.inspect
        end
      end
    end

    self.add_class SNMP
    UDP.bind_header SNMP, dport: SNMP::UDP_PORT1, sport: SNMP::UDP_PORT1
    UDP.bind_header SNMP, dport: SNMP::UDP_PORT2, sport: SNMP::UDP_PORT2
  end
end