# frozen_string_literal: true

# This file is part of RASN1
# See https://codeberg.org/lemontree55/rasn1 for more informations
# Copyright (C) 2016-2024 Sylvain Daubert <sylvain.daubert@laposte.net>
# Copyright (C) 2024 LemonTree55 <lenontree@proton.me>
# This program is published under MIT license.

module RASN1
  module Types
    # ASN.1 Integer
    #
    # An INTEGER is a primitive type to represent integers.
    # There is no limit on integer size.
    #
    # @example Simple Integers
    #   # Simple integer with a value associated
    #   int1 = RASN1::Types::Integer.new(value: 42)
    #   # Integer without value, but a default one
    #   int2 = RASN1::Types::Integer.new(default: -42)
    #
    # @example Enumerated Integer
    #   enum = RASN1::Types::Integer.new(enum: {one: 1, two: 2})
    #   enum.value = :two
    #   enum.value #=> :two
    #   enum.to_i  #=> 2
    # @author Sylvain Daubert
    # @author LemonTree55
    class Integer < Primitive
      # Associated enumeration
      # @return [Hash{String=>::Integer},nil]
      attr_reader :enum

      # Integer id value
      ID = 2

      # @!macro type_initialize
      # @option options [Hash{String=>::Integer}] :enum enumeration hash. Keys are names, and values
      #   are integers.
      # @raise [EnumeratedError] +:default+ value is unknown when +:enum+ key is present
      def initialize(options={})
        super
        initialize_enum(@options[:enum])
      end

      # Set integer value from a Ruby integer, or a String or a Symbol (only when {#enum} is defined).
      # @param val [::Integer,String,Symbol,nil]
      # @return [void]
      # @note
      #   An enumerated INTEGER may be set with an integer value out of enumeration,
      #   only when no value is set.
      # @raise [EnumeratedError] string/symbol value: no enum defined or +val+ not in enum
      # @raise [EnumeratedError] integer value: value already set and new value not in enum
      # @raise [TypeError] unsupported +val+ type
      # @since 0.16.2 raise TypeError (was EnumeratedError)
      def value=(val)
        @no_value = false
        case val
        when String, Symbol
          value_from_string_or_symbol(val)
        when ::Integer
          value_from_integer(val)
        when nil
          @no_value = true
          @value = void_value
        else
          raise TypeError, "#{val.class} not supported"
        end
      end

      # @return [::Integer]
      def void_value
        0
      end

      # Integer value
      # @return [::Integer]
      def to_i
        val = value? ? @value : @default
        case val
        when String, Symbol
          @enum[val]
        else
          val || 0
        end
      end

      # Make integer value from +der+ string.
      # @param der [String]
      # @param ber [::Boolean]
      # @return [void]
      def der_to_value(der, ber: false)
        int_value = der_to_int_value(der, ber: ber)
        @value = if @enum.empty?
                   int_value
                 else
                   int_to_enum(int_value)
                 end
      end

      private

      # Initialize integer with given enum hash.
      # @param enum [Hash{String=>::Integer}]
      # @return [void]
      def initialize_enum(enum)
        @enum = enum || {}
        return if @enum.empty?

        # To ensure @value has the correct type
        self.value = @value if value?

        check_enum_default
      end

      # Check defined default is coherent with enum
      # @return [void]
      # @raise [EnumeratedError] default value not defined in {#enum}
      # @raise [TypeError] unsupported type for default value
      def check_enum_default
        case @default
        when String, Symbol
          raise EnumeratedError, "#{@name}: unknwon enumerated default value #@{default}" unless @enum.key?(@default)
        when ::Integer
          raise EnumeratedError, "#{@name}: default value #@{default} not in enumeration" unless @enum.value?(@default)

          @default = @enum.key(@default)
        when nil
        else
          raise TypeError, "#{@name}: #{@default.class} not handled as default value"
        end
      end

      # Set value from given string or symbol value
      # @param val [String,Symbol]
      # @return [void]
      # @raise [EnumeratedError] no enum defined or +val+ not in enum
      def value_from_string_or_symbol(val)
        raise EnumeratedError, "#{@name} has no :enum" if @enum.empty?
        raise EnumeratedError, "#{@name}: unknwon enumerated value #{val}" unless @enum.key? val

        @value = val
      end

      # Set value from given integer value.
      # @param val [::Integer]
      # @return [void]
      # @raise [EnumeratedError] +val+ not in enum
      def value_from_integer(val)
        if @enum.empty?
          @value = val
        elsif @enum.value? val
          @value = @enum.key(val)
        else
          raise EnumeratedError, "#{@name}: #{val} not in enumeration"
        end
      end

      # @param value [::Integer]
      # @return [String]
      def int_value_to_der(value)
        size = compute_size(value)
        comp_value = value >= 0 ? value : (~(-value) + 1) & ((1 << (size * 8)) - 1)
        ary = comp_value.digits(256)
        # v is > 0 and its MSBit is 1. Add a 0 byte to mark it as positive
        ary << 0 if value.positive? && (value >> (size * 8 - 1) == 1)
        ary.reverse.pack('C*')
      end

      # Get size in bytes of +value+ integer
      # @param value [::Integer]
      # @return [::Integer]
      def compute_size(value)
        size = value.bit_length / 8 + ((value.bit_length % 8).positive? ? 1 : 0)
        size = 1 if size.zero?
        size
      end

      # @return [String]
      def value_to_der
        case @value
        when String, Symbol
          int_value_to_der(@enum[@value])
        when ::Integer
          int_value_to_der(@value)
        else
          raise TypeError, "#{@name}: #{@value.class} not handled"
        end
      end

      # @param der [String]
      # @param ber [::Boolean]
      # @return [::Integer]
      def der_to_int_value(der, ber: false) # rubocop:disable Lint/UnusedMethodArgument
        ary = der.unpack('C*')
        v = ary.reduce(0) { |len, b| (len << 8) | b }
        v = -((~v & ((1 << v.bit_length) - 1)) + 1) if ary[0] & 0x80 == 0x80
        v
      end

      # Return value to show  (enumerated if enum is defined, else integer)
      # @param int [::Integer]
      # @param check_enum [::Boolean]
      # @return [String,::Integer]
      # @raise [EnumeratedError] +check_enum+ is +true+, enum is defined, and +int+ is not in enum
      def int_to_enum(int, check_enum: true)
        raise EnumeratedError, "#{@name}: value #{int} not in enumeration" if check_enum && !@enum.value?(int)

        @enum.key(int) || int
      end

      # @return [::Integer]
      def explicit_type
        self.class.new(name: name, enum: enum)
      end

      # @return [String]
      def trace_data
        return super if explicit?
        return "    #{der_to_int_value(raw_data)} (0x#{bin2hex(raw_data)})" if @enum.empty?

        v = int_to_enum(der_to_int_value(raw_data), check_enum: false)
        "    #{v} (0x#{bin2hex(raw_data)})"
      end
    end
  end
end
