defmodule DaProductApp.MercuryISO8583.Bitmap do @moduledoc """ Binary bitmap parsing and generation for ISO8583 fields 0-127. Handles both primary (fields 1-64) and secondary (fields 65-128) bitmaps. """ import Bitwise alias DaProductApp.MercuryISO8583.Utils @doc """ Parse binary bitmap to get list of present field numbers. ## Examples iex> Bitmap.parse(<<0x82, 0x38, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00>>) {:ok, ["1", "7", "11", "12", "13", "70"]} """ def parse(bitmap) when is_binary(bitmap) do case byte_size(bitmap) do 8 -> parse_primary_bitmap(bitmap) 16 -> parse_extended_bitmap(bitmap) size when size >= 8 -> # Take first 8 or 16 bytes depending on field 1 bit <> = bitmap if (first_byte &&& 0x80) != 0 do # Field 1 is set, indicating secondary bitmap present case byte_size(bitmap) do size when size >= 16 -> <> = bitmap parse_extended_bitmap(primary <> secondary) _ -> {:error, "Insufficient bitmap data for secondary bitmap"} end else # Only primary bitmap <> = bitmap parse_primary_bitmap(primary) end _ -> {:error, "Invalid bitmap size: #{byte_size(bitmap)} bytes"} end end @doc """ Parse primary bitmap (fields 1-64). """ defp parse_primary_bitmap(<>) do fields = extract_field_numbers(primary, 1, 64) {:ok, fields} end @doc """ Parse extended bitmap (fields 1-128). """ defp parse_extended_bitmap(<>) do primary_fields = extract_field_numbers(primary, 1, 64) secondary_fields = extract_field_numbers(secondary, 65, 128) # Remove field 1 from primary fields since it indicates secondary bitmap presence filtered_primary = Enum.reject(primary_fields, &(&1 == "1")) all_fields = filtered_primary ++ secondary_fields {:ok, all_fields} end @doc """ Extract field numbers from bitmap binary. """ defp extract_field_numbers(bitmap, start_field, end_field) do bitmap |> :binary.bin_to_list() |> Enum.with_index() |> Enum.flat_map(fn {byte, byte_index} -> 0..7 |> Enum.filter(fn bit_index -> (byte &&& (1 <<< (7 - bit_index))) != 0 end) |> Enum.map(fn bit_index -> field_number = start_field + (byte_index * 8) + bit_index if field_number <= end_field do Integer.to_string(field_number) else nil end end) |> Enum.filter(&(&1 != nil)) end) end @doc """ Generate bitmap from list of field numbers. ## Examples iex> Bitmap.generate(["7", "11", "12", "13", "70"]) {:ok, <<0x82, 0x38, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00>>} """ def generate(field_list) when is_list(field_list) do field_numbers = field_list |> Enum.map(&Utils.field_to_integer/1) |> Enum.filter(&(&1 >= 1 and &1 <= 128)) |> Enum.sort() |> Enum.uniq() max_field = Enum.max(field_numbers, fn -> 0 end) cond do max_field == 0 -> {:ok, <<0, 0, 0, 0, 0, 0, 0, 0>>} max_field <= 64 -> generate_primary_bitmap(field_numbers) max_field <= 128 -> generate_extended_bitmap(field_numbers) true -> {:error, "Field number #{max_field} exceeds maximum of 128"} end end def generate(field_map) when is_map(field_map) do field_list = Map.keys(field_map) generate(field_list) end @doc """ Generate primary bitmap (fields 1-64 only). """ defp generate_primary_bitmap(field_numbers) do bitmap = generate_bitmap_bytes(field_numbers, 1, 64) {:ok, bitmap} end @doc """ Generate extended bitmap (fields 1-128). """ defp generate_extended_bitmap(field_numbers) do # Add field 1 to indicate secondary bitmap presence extended_fields = [1 | field_numbers] primary_fields = Enum.filter(extended_fields, &(&1 <= 64)) secondary_fields = Enum.filter(field_numbers, &(&1 > 64)) primary_bitmap = generate_bitmap_bytes(primary_fields, 1, 64) secondary_bitmap = generate_bitmap_bytes(secondary_fields, 65, 128) {:ok, primary_bitmap <> secondary_bitmap} end @doc """ Generate bitmap bytes for specified field range. """ defp generate_bitmap_bytes(field_numbers, start_field, end_field) do bitmap_size = div(end_field - start_field + 1, 8) initial_bitmap = :binary.copy(<<0>>, bitmap_size) Enum.reduce(field_numbers, initial_bitmap, fn field_num, bitmap_acc -> if field_num >= start_field and field_num <= end_field do set_bitmap_bit(bitmap_acc, field_num, start_field) else bitmap_acc end end) end @doc """ Set bit in bitmap for given field number. """ defp set_bitmap_bit(bitmap, field_number, base_field) do # Calculate position relative to base field relative_position = field_number - base_field byte_index = div(relative_position, 8) bit_index = rem(relative_position, 8) if byte_index < byte_size(bitmap) do <> = bitmap new_byte = byte ||| (1 <<< (7 - bit_index)) prefix <> <> <> suffix else bitmap end end @doc """ Check if field is present in bitmap. ## Examples iex> Bitmap.field_present?(bitmap, "11") true """ def field_present?(bitmap, field) when is_binary(bitmap) do case parse(bitmap) do {:ok, field_list} -> field_str = Utils.field_to_string(field) field_str in field_list {:error, _} -> false end end @doc """ Add field to existing bitmap. ## Examples iex> Bitmap.add_field(bitmap, "70") {:ok, updated_bitmap} """ def add_field(bitmap, field) when is_binary(bitmap) do case parse(bitmap) do {:ok, field_list} -> field_str = Utils.field_to_string(field) updated_fields = [field_str | field_list] |> Enum.uniq() generate(updated_fields) {:error, reason} -> {:error, reason} end end @doc """ Remove field from existing bitmap. ## Examples iex> Bitmap.remove_field(bitmap, "70") {:ok, updated_bitmap} """ def remove_field(bitmap, field) when is_binary(bitmap) do case parse(bitmap) do {:ok, field_list} -> field_str = Utils.field_to_string(field) updated_fields = Enum.reject(field_list, &(&1 == field_str)) generate(updated_fields) {:error, reason} -> {:error, reason} end end @doc """ Convert bitmap to hex string representation. ## Examples iex> Bitmap.to_hex(<<0x82, 0x38, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00>>) "8238000000000400" """ def to_hex(bitmap) when is_binary(bitmap) do Utils.binary_to_hex(bitmap) end @doc """ Convert hex string to bitmap binary. ## Examples iex> Bitmap.from_hex("8238000000000400") {:ok, <<0x82, 0x38, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00>>} """ def from_hex(hex_string) when is_binary(hex_string) do try do bitmap = Utils.hex_to_binary(hex_string) {:ok, bitmap} rescue _ -> {:error, "Invalid hex string"} end end @doc """ Get bitmap information including field count and type. ## Examples iex> Bitmap.info(bitmap) %{ size_bytes: 8, type: :primary, field_count: 6, fields: ["7", "11", "12", "13", "70"] } """ def info(bitmap) when is_binary(bitmap) do case parse(bitmap) do {:ok, field_list} -> bitmap_type = if byte_size(bitmap) >= 16, do: :extended, else: :primary %{ size_bytes: byte_size(bitmap), type: bitmap_type, field_count: length(field_list), fields: field_list } {:error, reason} -> %{ size_bytes: byte_size(bitmap), type: :invalid, field_count: 0, fields: [], error: reason } end end @doc """ Merge two bitmaps by combining their set fields. ## Examples iex> Bitmap.merge(bitmap1, bitmap2) {:ok, merged_bitmap} """ def merge(bitmap1, bitmap2) when is_binary(bitmap1) and is_binary(bitmap2) do with {:ok, fields1} <- parse(bitmap1), {:ok, fields2} <- parse(bitmap2) do combined_fields = (fields1 ++ fields2) |> Enum.uniq() generate(combined_fields) end end @doc """ Create empty bitmap of specified size. ## Examples iex> Bitmap.empty(:primary) <<0, 0, 0, 0, 0, 0, 0, 0>> iex> Bitmap.empty(:extended) <<0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0>> """ def empty(:primary), do: <<0, 0, 0, 0, 0, 0, 0, 0>> def empty(:extended), do: <<0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0>> def empty(_), do: empty(:primary) end