require 'bindata/base_primitive'

module BinData
  # Defines a number of classes that contain a bit based integer.
  # The integer is defined by endian and number of bits.

  module BitField #:nodoc: all
    class << self
      def define_class(name, nbits, endian, signed = :unsigned)
        unless BinData.const_defined?(name)
          BinData.module_eval <<-END
            class #{name} < BinData::BasePrimitive
              # nbits is either an integer or the symbol `:nbits`
              BitField.define_methods(self, #{nbits.inspect}, :#{endian}, :#{signed})
            end
          END
        end

        BinData.const_get(name)
      end

      def define_methods(bit_class, nbits, endian, signed)
        bit_class.module_eval <<-END
          #{create_params_code(nbits)}

          def assign(val)
            #{create_nbits_code(nbits)}
            #{create_clamp_code(nbits, signed)}
            super(val)
          end

          def do_write(io)
            #{create_nbits_code(nbits)}
            val = _value
            #{create_int2uint_code(nbits, signed)}
            io.writebits(val, #{nbits}, :#{endian})
          end

          def do_num_bytes
            #{create_nbits_code(nbits)}
            #{create_do_num_bytes_code(nbits)}
          end

          def bit_aligned?
            true
          end

          #---------------
          private

          def read_and_return_value(io)
            #{create_nbits_code(nbits)}
            val = io.readbits(#{nbits}, :#{endian})
            #{create_uint2int_code(nbits, signed)}
            val
          end

          def sensible_default
            0
          end
        END
      end

      def create_params_code(nbits)
        if nbits == :nbits
          "mandatory_parameter :nbits"
        else
          ""
        end
      end

      def create_nbits_code(nbits)
        if nbits == :nbits
          "nbits = eval_parameter(:nbits)"
        else
          ""
        end
      end

      def create_do_num_bytes_code(nbits)
        if nbits == :nbits
          "nbits / 8.0"
        else
          nbits / 8.0
        end
      end

      def create_clamp_code(nbits, signed)
        if nbits == :nbits
          create_dynamic_clamp_code(signed)
        else
          create_fixed_clamp_code(nbits, signed)
        end
      end

      def create_dynamic_clamp_code(signed)
        if signed == :signed
          max = "max = (1 << (nbits - 1)) - 1"
          min = "min = -(max + 1)"
        else
          max = "max = (1 << nbits) - 1"
          min = "min = 0"
        end

        "#{max}; #{min}; val = (val < min) ? min : (val > max) ? max : val"
      end

      def create_fixed_clamp_code(nbits, signed)
        if nbits == 1 && signed == :signed
          raise "signed bitfield must have more than one bit"
        end

        if signed == :signed
          max = (1 << (nbits - 1)) - 1
          min = -(max + 1)
        else
          min = 0
          max = (1 << nbits) - 1
        end

        clamp = "(val < #{min}) ? #{min} : (val > #{max}) ? #{max} : val"

        if nbits == 1
          # allow single bits to be used as booleans
          clamp = "(val == true) ? 1 : (not val) ? 0 : #{clamp}"
        end

        "val = #{clamp}"
      end

      def create_int2uint_code(nbits, signed)
        if signed != :signed
          ""
        elsif nbits == :nbits
          "val &= (1 << nbits) - 1"
        else
          "val &= #{(1 << nbits) - 1}"
        end
      end

      def create_uint2int_code(nbits, signed)
        if signed != :signed
          ""
        elsif nbits == :nbits
          "val -= (1 << nbits) if (val >= (1 << (nbits - 1)))"
        else
          "val -= #{1 << nbits} if (val >= #{1 << (nbits - 1)})"
        end
      end
    end
  end

  # Create classes for dynamic bitfields
  {
    "Bit"    => :big,
    "BitLe"  => :little,
    "Sbit"   => [:big, :signed],
    "SbitLe" => [:little, :signed],
  }.each_pair { |name, args| BitField.define_class(name, :nbits, *args) }

  # Create classes on demand
  module BitFieldFactory
    def const_missing(name)
      mappings = {
        /^Bit(\d+)$/    => :big,
        /^Bit(\d+)le$/  => :little,
        /^Sbit(\d+)$/   => [:big, :signed],
        /^Sbit(\d+)le$/ => [:little, :signed]
      }

      mappings.each_pair do |regex, args|
        if regex =~ name.to_s
          nbits = $1.to_i
          return BitField.define_class(name, nbits, *args)
        end
      end

      super(name)
    end
  end
  BinData.extend BitFieldFactory
end
